feat(stage-web): module consciousness setting
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"@vueuse/shared": "^12.8.2",
|
||||
"@xsai-ext/providers-cloud": "catalog:",
|
||||
"@xsai-ext/providers-local": "catalog:",
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"@xsai/generate-text": "catalog:",
|
||||
|
||||
@@ -78,6 +78,28 @@ settings:
|
||||
theme: Theme
|
||||
title: Settings
|
||||
voices: Voice
|
||||
modules:
|
||||
consciousness:
|
||||
provider-model-selection:
|
||||
title: Model
|
||||
subtitle: Select a model from the provider
|
||||
loading: Loading available models...
|
||||
error: Error loading models
|
||||
no_models: No models available
|
||||
no_models_description: No models were found for this provider
|
||||
not_supported: Model listing not supported
|
||||
not_supported_description: This provider doesn't support model listing functionality
|
||||
custom_model_placeholder: Enter custom model name...
|
||||
manual_model_name: Model Name
|
||||
manual_model_placeholder: Enter the model name to use with this provider
|
||||
show_more: Show more
|
||||
show_less: Show less
|
||||
search_placeholder: Search models...
|
||||
search_results: 'Found {count} of {total} models'
|
||||
no_search_results: No matching models
|
||||
no_search_results_description: "No models match '{query}'. Try a different search term."
|
||||
expand: Expand
|
||||
collapse: Collapse
|
||||
stage:
|
||||
chat:
|
||||
message:
|
||||
@@ -115,3 +137,43 @@ stage:
|
||||
model:
|
||||
title: Model
|
||||
waiting: Waiting
|
||||
providers:
|
||||
openrouter:
|
||||
name: OpenRouter
|
||||
description: openrouter.ai
|
||||
openai:
|
||||
name: OpenAI
|
||||
description: openai.com
|
||||
ollama:
|
||||
name: Ollama
|
||||
description: ollama.ai
|
||||
vllm:
|
||||
name: vLLM
|
||||
description: vllm.ai
|
||||
elevenlabs:
|
||||
name: ElevenLabs
|
||||
description: elevenlabs.io
|
||||
xai:
|
||||
name: xAI
|
||||
description: x.ai
|
||||
deepseek:
|
||||
name: DeepSeek
|
||||
description: deepseek.com
|
||||
together:
|
||||
name: Together.ai
|
||||
description: together.ai
|
||||
novita:
|
||||
name: Novita
|
||||
description: novita.ai
|
||||
fireworks:
|
||||
name: Fireworks.ai
|
||||
description: fireworks.ai
|
||||
cloudflare:
|
||||
name: Cloudflare Workers AI
|
||||
description: cloudflare.com
|
||||
mistral:
|
||||
name: Mistral
|
||||
description: mistral.ai
|
||||
moonshot:
|
||||
name: Moonshot AI
|
||||
description: moonshot.ai
|
||||
|
||||
@@ -65,6 +65,28 @@ settings:
|
||||
theme: 主题颜色
|
||||
title: 设置
|
||||
voices: 声线
|
||||
modules:
|
||||
consciousness:
|
||||
provider-model-selection:
|
||||
title: 模型
|
||||
subtitle: 选择一个模型
|
||||
loading: 获取模型中...
|
||||
error: 获取出错啦
|
||||
no_models: 没有任何可用的模型
|
||||
no_models_description: 该提供商的模型列表没有返回任何的数据
|
||||
not_supported: 无法列出模型
|
||||
not_supported_description: 此提供商不支持列出模型的功能
|
||||
custom_model_placeholder: 输入自订模型名字
|
||||
manual_model_name: 模型名字
|
||||
manual_model_placeholder: 输入与该提供商一起使用的模型名字
|
||||
show_more: Show more
|
||||
show_less: Show less
|
||||
search_placeholder: Search models...
|
||||
search_results: 'Found {count} of {total} models'
|
||||
no_search_results: No matching models
|
||||
no_search_results_description: "No models match '{query}'. Try a different search term."
|
||||
expand: Expand
|
||||
collapse: Collapse
|
||||
stage:
|
||||
chat:
|
||||
message:
|
||||
@@ -89,3 +111,43 @@ stage:
|
||||
debug-menu:
|
||||
emotions: 表情
|
||||
waiting: 等待中
|
||||
providers:
|
||||
openrouter:
|
||||
name: OpenRouter
|
||||
description: openrouter.ai
|
||||
openai:
|
||||
name: OpenAI
|
||||
description: openai.com
|
||||
ollama:
|
||||
name: Ollama
|
||||
description: ollama.ai
|
||||
vllm:
|
||||
name: vLLM
|
||||
description: vllm.ai
|
||||
elevenlabs:
|
||||
name: ElevenLabs
|
||||
description: elevenlabs.io
|
||||
xai:
|
||||
name: xAI
|
||||
description: x.ai
|
||||
deepseek:
|
||||
name: DeepSeek
|
||||
description: deepseek.com
|
||||
together:
|
||||
name: Together.ai
|
||||
description: together.ai
|
||||
novita:
|
||||
name: Novita
|
||||
description: novita.ai
|
||||
fireworks:
|
||||
name: Fireworks.ai
|
||||
description: fireworks.ai
|
||||
cloudflare:
|
||||
name: Cloudflare Workers AI
|
||||
description: cloudflare.com
|
||||
mistral:
|
||||
name: Mistral
|
||||
description: mistral.ai
|
||||
moonshot:
|
||||
name: Moonshot AI
|
||||
description: moonshot.ai
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"@vueuse/shared": "^12.8.2",
|
||||
"@xsai-ext/providers-cloud": "catalog:",
|
||||
"@xsai-ext/providers-local": "catalog:",
|
||||
"@xsai/generate-speech": "catalog:",
|
||||
"@xsai/generate-text": "catalog:",
|
||||
|
||||
@@ -59,15 +59,17 @@ defineProps<{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
width: 0%;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
transition: all 0.35s ease-in-out;
|
||||
mask-image: linear-gradient(120deg, white 30%, transparent 50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menu-icon-item:hover::before {
|
||||
--at-apply: 'bg-gradient-to-r from-primary-500/20 via-primary-500/10 to-transparent dark:from-primary-400/20 dark:via-primary-400/10 dark:to-transparent';
|
||||
width: 85%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-icon-item::after {
|
||||
|
||||
@@ -93,15 +93,17 @@ const props = defineProps<{
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 0%;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
transition: all 0.35s ease-in-out;
|
||||
mask-image: linear-gradient(120deg, white 100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menu-icon-status-item:hover::before {
|
||||
--at-apply: 'bg-gradient-to-r from-primary-500/20 via-primary-500/10 to-transparent dark:from-primary-400/20 dark:via-primary-400/10 dark:to-transparent';
|
||||
width: 50%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-icon-status-item-link::after {
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RadioCardDetail, RadioCardSimple } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/settings/modules'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
const isModelListExpanded = ref(false)
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { availableProviders } = storeToRefs(providersStore)
|
||||
const {
|
||||
activeProvider,
|
||||
activeModel,
|
||||
customModelName,
|
||||
modelSearchQuery,
|
||||
availableProvidersMetadata,
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
filteredModels,
|
||||
} = storeToRefs(consciousnessStore)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(async () => {
|
||||
await consciousnessStore.loadModelsForProvider(activeProvider.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,43 +45,249 @@ const router = useRouter()
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4>
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
Provider
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>Select the suitable LLM provider for consciousness</span>
|
||||
<div bg="neutral-100 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-4">
|
||||
<div>
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
Provider
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>Select the suitable LLM provider for consciousness</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div max-w-full>
|
||||
<!--
|
||||
<div max-w-full>
|
||||
<!--
|
||||
fieldset has min-width set to --webkit-min-container, in order to use over flow scroll,
|
||||
we need to set the min-width to 0.
|
||||
See also: https://stackoverflow.com/a/33737340
|
||||
-->
|
||||
<fieldset flex="~ row gap-4" :style="{ 'scrollbar-width': 'none' }" min-w-0 of-x-scroll scroll-smooth>
|
||||
<label
|
||||
v-for="(_, index) in 10"
|
||||
:key="index"
|
||||
border="2px solid"
|
||||
:class="[
|
||||
index === 0 ? 'bg-primary-50/50 dark:bg-primary-900' : 'bg-white dark:bg-neutral-900',
|
||||
index === 0 ? 'border-primary-300 dark:border-primary-800' : 'border-neutral-200 dark:border-neutral-800',
|
||||
]"
|
||||
flex="~ col" block min-w-50 w-fit cursor-pointer items-start rounded-lg p-3 text-left
|
||||
<fieldset
|
||||
v-if="availableProviders.length > 0" flex="~ row gap-4" :style="{ 'scrollbar-width': 'none' }"
|
||||
min-w-0 of-x-scroll scroll-smooth role="radiogroup"
|
||||
>
|
||||
<input type="radio" name="provider" value="provider-1">
|
||||
<div flex="~ col" min-h-16 w-full items-center justify-center>
|
||||
Provider {{ index + 1 }}
|
||||
<RadioCardSimple
|
||||
v-for="metadata in availableProvidersMetadata" :id="metadata.id" :key="metadata.id"
|
||||
v-model="activeProvider" name="provider" :value="metadata.id"
|
||||
:title="metadata.localizedName" :description="metadata.localizedDescription"
|
||||
/>
|
||||
</fieldset>
|
||||
<div v-else>
|
||||
<RouterLink
|
||||
class="flex items-center gap-3 rounded-lg p-4" border="2 dashed neutral-200 dark:neutral-800"
|
||||
bg="neutral-50 dark:neutral-800" transition="colors duration-200 ease-in-out" to="/settings/providers"
|
||||
>
|
||||
<div i-solar:warning-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">No Providers Configured</span>
|
||||
<span class="text-sm text-neutral-400 dark:text-neutral-500">Click here to set up your LLM
|
||||
providers</span>
|
||||
</div>
|
||||
<div i-solar:arrow-right-line-duotone class="ml-auto text-xl text-neutral-400 dark:text-neutral-500" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model selection section -->
|
||||
<div v-if="activeProvider && supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ $t('settings.modules.consciousness.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ $t('settings.modules.consciousness.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
|
||||
<div class="mr-2 animate-spin">
|
||||
<div i-solar:spinner-line-duotone text-xl />
|
||||
</div>
|
||||
<span>{{ $t('settings.modules.consciousness.provider-model-selection.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div
|
||||
v-else-if="activeProviderModelError"
|
||||
class="flex items-center gap-3 border border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
|
||||
>
|
||||
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ $t('settings.modules.consciousness.provider-model-selection.error') }}</span>
|
||||
<span class="text-sm text-red-600 dark:text-red-400">{{ activeProviderModelError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No models available -->
|
||||
<div
|
||||
v-else-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
|
||||
class="flex items-center gap-3 border border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ $t('settings.modules.consciousness.provider-model-selection.no_models')
|
||||
}}</span>
|
||||
<span class="text-sm text-amber-600 dark:text-amber-400">{{
|
||||
$t('settings.modules.consciousness.provider-model-selection.no_models_description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search bar (only show if we have models) -->
|
||||
<template v-else-if="providerModels.length > 0">
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<div i-solar:magnifer-line-duotone class="text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<input
|
||||
v-model="modelSearchQuery" type="search" class="w-full rounded-lg p-2.5 pl-10 text-sm outline-none"
|
||||
border="focus:primary-500 dark:focus:primary-400 ~ neutral-300 dark:neutral-700 2"
|
||||
transition="all duration-200 ease-in-out"
|
||||
ring="focus:primary-500 dark:focus:primary-400 0 focus:2 focus:offset-0 focus:opacity-50"
|
||||
bg="white dark:neutral-900"
|
||||
:placeholder="$t('settings.modules.consciousness.provider-model-selection.search_placeholder')"
|
||||
>
|
||||
<button
|
||||
v-if="modelSearchQuery" type="button" class="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
@click="modelSearchQuery = ''"
|
||||
>
|
||||
<div
|
||||
i-solar:close-circle-line-duotone
|
||||
class="text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Models list with search results info -->
|
||||
<div class="space-y-2">
|
||||
<!-- Search results info -->
|
||||
<div v-if="modelSearchQuery" class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ $t('settings.modules.consciousness.provider-model-selection.search_results', {
|
||||
count: filteredModels.length,
|
||||
total: providerModels.length,
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
<!-- No search results -->
|
||||
<div
|
||||
v-if="modelSearchQuery && filteredModels.length === 0"
|
||||
class="flex items-center gap-3 border border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{
|
||||
$t('settings.modules.consciousness.provider-model-selection.no_search_results') }}</span>
|
||||
<span class="text-sm text-amber-600 dark:text-amber-400">
|
||||
{{ $t('settings.modules.consciousness.provider-model-selection.no_search_results_description', {
|
||||
query: modelSearchQuery }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Models grid -->
|
||||
<div class="relative">
|
||||
<!-- Horizontally scrollable container -->
|
||||
<div
|
||||
class="scrollbar-hide grid auto-cols-[350px] grid-flow-col gap-4 overflow-x-auto pb-4"
|
||||
:class="[
|
||||
isModelListExpanded ? 'md:grid-cols-2 md:grid-flow-row md:auto-cols-auto' : '',
|
||||
]"
|
||||
transition="all duration-200 ease-in-out"
|
||||
style="scroll-snap-type: x mandatory;"
|
||||
>
|
||||
<RadioCardDetail
|
||||
v-for="model in filteredModels"
|
||||
:id="model.id"
|
||||
:key="model.id"
|
||||
v-model="activeModel"
|
||||
:value="model.id"
|
||||
:title="model.name"
|
||||
:description="model.description"
|
||||
:deprecated="model.deprecated"
|
||||
:show-expand-collapse="true"
|
||||
:expand-collapse-threshold="100"
|
||||
:show-custom-input="model.id === 'custom'"
|
||||
:custom-input-value="customModelName"
|
||||
:custom-input-placeholder="$t('settings.modules.consciousness.provider-model-selection.custom_model_placeholder')"
|
||||
name="model"
|
||||
class="scroll-snap-align-start"
|
||||
@update:custom-input-value="customModelName = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Expand/collapse handle -->
|
||||
<div
|
||||
bg="neutral-100 dark:[rgba(0,0,0,0.3)]"
|
||||
rounded-xl
|
||||
:class="[
|
||||
isModelListExpanded ? 'fixed bottom-4 left-1/2 translate-x--1/2 z-10 w-[calc(100%-16px-40px-16px)]' : 'mt-0 w-full rounded-lg',
|
||||
]"
|
||||
>
|
||||
<button
|
||||
w-full
|
||||
flex items-center justify-center gap-2 rounded-lg py-2 transition="all duration-200 ease-in-out"
|
||||
@click="isModelListExpanded = !isModelListExpanded"
|
||||
:class="[
|
||||
isModelListExpanded ? 'bg-primary-500 hover:bg-primary-600 text-white' : 'bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700',
|
||||
]"
|
||||
>
|
||||
<span>{{ isModelListExpanded ? $t('settings.modules.consciousness.provider-model-selection.collapse') : $t('settings.modules.consciousness.provider-model-selection.expand') }}</span>
|
||||
<div
|
||||
:class="isModelListExpanded ? 'rotate-180' : ''"
|
||||
i-solar:alt-arrow-down-bold-duotone transition="transform duration-200 ease-in-out"
|
||||
class="text-lg"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider doesn't support model listing -->
|
||||
<div v-else-if="activeProvider && !supportsModelListing">
|
||||
<div flex="~ col gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg md:text-2xl">
|
||||
{{ $t('settings.modules.consciousness.provider-model-selection.title') }}
|
||||
</h2>
|
||||
<div text="neutral-400 dark:neutral-400">
|
||||
<span>{{ $t('settings.modules.consciousness.provider-model-selection.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-primary-50 border-primary-200 dark:bg-primary-900/20 dark:border-primary-800 flex items-center gap-3 border rounded-lg p-4"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-primary-500 dark:text-primary-400 text-2xl" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ $t('settings.modules.consciousness.provider-model-selection.not_supported')
|
||||
}}</span>
|
||||
<span class="dark:text-primary-400 text-primary-600 text-sm">{{
|
||||
$t('settings.modules.consciousness.provider-model-selection.not_supported_description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual model input for providers without model listing -->
|
||||
<div class="mt-2">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
{{ $t('settings.modules.consciousness.provider-model-selection.manual_model_name') }}
|
||||
</label>
|
||||
</fieldset>
|
||||
<input
|
||||
v-model="activeModel" type="text"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
:placeholder="$t('settings.modules.consciousness.provider-model-selection.manual_model_placeholder')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
|
||||
|
||||
<div fixed bottom-0 right-0 z--1 class="text-neutral-100/80 dark:text-neutral-500/20">
|
||||
<div text="40" i-lucide:ghost translate-x-10 translate-y-10 />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,132 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconStatusItem from '../../../components/Menu/IconStatusItem.vue'
|
||||
|
||||
interface ModelProvider {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
iconImage?: string
|
||||
to: string
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { configuredForOpenRouter } = storeToRefs(providersStore)
|
||||
|
||||
const providersList = computed<ModelProvider[]>(() => [
|
||||
{
|
||||
id: 'openrouter-ai',
|
||||
name: 'OpenRouter',
|
||||
description: 'openrouter.ai',
|
||||
icon: 'i-lobe-icons:openrouter',
|
||||
to: '/settings/providers/openrouter',
|
||||
configured: configuredForOpenRouter.value,
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
description: 'openai.com',
|
||||
icon: 'i-lobe-icons:openai',
|
||||
to: '/settings/providers/openai',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'ollama-ai',
|
||||
name: 'Ollama',
|
||||
description: 'ollama.com',
|
||||
icon: 'i-lobe-icons:ollama',
|
||||
to: '/settings/providers/ollama',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'vllm',
|
||||
name: 'vLLM',
|
||||
description: 'vllm.ai',
|
||||
iconColor: 'i-lobe-icons:vllm-color',
|
||||
to: '/settings/providers/vllm',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'elevenlabs',
|
||||
name: 'ElevenLabs',
|
||||
description: 'elevenlabs.io',
|
||||
icon: 'i-simple-icons:elevenlabs',
|
||||
to: '/settings/providers/elevenlabs',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'xai',
|
||||
name: 'xAI',
|
||||
description: 'x.ai',
|
||||
icon: 'i-lobe-icons:xai',
|
||||
to: '/settings/providers/xai',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
description: 'deepseek.com',
|
||||
iconColor: 'i-lobe-icons:deepseek-color',
|
||||
to: '/settings/providers/deepseek',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'together-ai',
|
||||
name: 'Together.ai',
|
||||
description: 'together.ai',
|
||||
iconColor: 'i-lobe-icons:together-color',
|
||||
to: '/settings/providers/together',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'novita-ai',
|
||||
name: 'Novita',
|
||||
description: 'novita.ai',
|
||||
iconColor: 'i-lobe-icons:novita-color',
|
||||
to: '/settings/providers/novita',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'fireworks-ai',
|
||||
name: 'Fireworks.ai',
|
||||
description: 'fireworks.ai',
|
||||
icon: 'i-lobe-icons:fireworks',
|
||||
to: '/settings/providers/fireworks',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'cloudflare-workers-ai',
|
||||
name: 'Cloudflare Workers AI',
|
||||
description: 'cloudflare.com',
|
||||
iconColor: 'i-lobe-icons:cloudflare-color',
|
||||
to: '/settings/providers/cloudflare',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'mistral-ai',
|
||||
name: 'Mistral',
|
||||
description: 'mistral.ai',
|
||||
iconColor: 'i-lobe-icons:mistral-color',
|
||||
to: '/settings/providers/mistral',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'moonshot-ai',
|
||||
name: 'Moonshot AI',
|
||||
description: 'moonshot.ai',
|
||||
icon: 'i-lobe-icons:moonshot',
|
||||
to: '/settings/providers/moonshot',
|
||||
configured: false,
|
||||
},
|
||||
])
|
||||
const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -145,14 +26,14 @@ const providersList = computed<ModelProvider[]>(() => [
|
||||
</div>
|
||||
<div grid="~ cols-2 gap-2">
|
||||
<IconStatusItem
|
||||
v-for="provider in providersList"
|
||||
v-for="provider in allProvidersMetadata"
|
||||
:key="provider.id"
|
||||
:title="provider.name"
|
||||
:description="provider.description"
|
||||
:title="provider.localizedName"
|
||||
:description="provider.localizedDescription"
|
||||
:icon="provider.icon"
|
||||
:icon-color="provider.iconColor"
|
||||
:icon-image="provider.iconImage"
|
||||
:to="provider.to"
|
||||
:to="`/settings/providers/${provider.id.replace('-ai', '')}`"
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useToggle } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
const apiKey = ref(providers.value.openai?.apiKey || '')
|
||||
const baseUrl = ref(providers.value.openai?.baseUrl || '')
|
||||
|
||||
const advancedVisible = ref(false)
|
||||
const toggleAdvancedVisible = useToggle(advancedVisible)
|
||||
|
||||
onMounted(() => {
|
||||
if (!providers.value.openai) {
|
||||
providers.value.openai = {
|
||||
baseUrl: 'https://api.openai.com/v1/',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value.openai = {
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || 'https://api.openai.com/v1/',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 relative>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]">
|
||||
<span text="neutral-300 dark:neutral-500">Provider</span>
|
||||
</div>
|
||||
<div text-3xl font-semibold>
|
||||
OpenAI
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<form flex="~ col gap-4" @submit.prevent="() => {}">
|
||||
<div>
|
||||
<h2 text-2xl>
|
||||
<span>Basic</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<label grid="~ cols-2 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
API Key
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
|
||||
API Key for OpenAI
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="apiKey" type="password"
|
||||
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
|
||||
transition="border duration-250 ease-in-out"
|
||||
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
|
||||
placeholder="sk-..."
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<Collapsable w-full>
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
|
||||
>
|
||||
<h2 text-2xl>
|
||||
<span>Advanced</span>
|
||||
</h2>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div mt-4>
|
||||
<label grid="~ cols-2 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
Base URL
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Custom base URL (optional)
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="baseUrl" type="text"
|
||||
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
|
||||
transition="border duration-250 ease-in-out"
|
||||
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
|
||||
placeholder="https://api.openai.com/v1/"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</form>
|
||||
<div fixed bottom-0 right-0 text="neutral-100/80 dark:neutral-500/20">
|
||||
<div text="40" i-lobe-icons:openai translate-x-10 translate-y-10 />
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,31 +3,35 @@ import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useToggle } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
const apiKey = ref(providers.value['openrouter-ai']?.apiKey || '')
|
||||
const baseUrl = ref(providers.value['openrouter-ai']?.baseUrl || '')
|
||||
// Get provider metadata
|
||||
const providerId = 'openrouter-ai'
|
||||
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
|
||||
|
||||
const apiKey = ref(providers.value[providerId]?.apiKey || '')
|
||||
const baseUrl = ref(providers.value[providerId]?.baseUrl || '')
|
||||
|
||||
const advancedVisible = ref(false)
|
||||
const toggleAdvancedVisible = useToggle(advancedVisible)
|
||||
|
||||
onMounted(() => {
|
||||
if (!providers.value['openrouter-ai']) {
|
||||
providers.value['openrouter-ai'] = {
|
||||
baseUrl: 'https://openrouter.ai/api/v1/',
|
||||
}
|
||||
}
|
||||
providersStore.initializeProvider(providerId)
|
||||
|
||||
// Initialize refs with current values
|
||||
apiKey.value = providers.value[providerId]?.apiKey || ''
|
||||
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.baseUrlDefault || ''
|
||||
})
|
||||
|
||||
watch([apiKey, baseUrl], () => {
|
||||
providers.value['openrouter-ai'] = {
|
||||
providers.value[providerId] = {
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value || 'https://openrouter.ai/api/v1/',
|
||||
baseUrl: baseUrl.value || providerMetadata.value?.baseUrlDefault || '',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -42,7 +46,7 @@ watch([apiKey, baseUrl], () => {
|
||||
<span text="neutral-300 dark:neutral-500">Provider</span>
|
||||
</div>
|
||||
<div text-3xl font-semibold>
|
||||
OpenRouter
|
||||
{{ providerMetadata?.localizedName }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -60,7 +64,7 @@ watch([apiKey, baseUrl], () => {
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
|
||||
API Key for OpenRouter
|
||||
API Key for {{ providerMetadata?.localizedName }}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
@@ -103,13 +107,13 @@ watch([apiKey, baseUrl], () => {
|
||||
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
|
||||
transition="border duration-250 ease-in-out"
|
||||
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
|
||||
placeholder="https://openrouter.ai/api/v1/"
|
||||
:placeholder="providerMetadata?.baseUrlDefault"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</form>
|
||||
<div fixed bottom-0 right-0 text="neutral-100/80 dark:neutral-500/20">
|
||||
<div text="40" i-lobe-icons:openrouter translate-x-10 translate-y-10 />
|
||||
<div text="40" :class="providerMetadata?.icon" translate-x-10 translate-y-10 />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionVertical } from '@proj-airi/stage-ui/components'
|
||||
import { ref } from 'vue'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
title: string
|
||||
description?: string
|
||||
modelValue: string
|
||||
deprecated?: boolean
|
||||
showExpandCollapse?: boolean
|
||||
expandCollapseThreshold?: number
|
||||
customInputValue?: string
|
||||
customInputPlaceholder?: string
|
||||
showCustomInput?: boolean
|
||||
}>(), {
|
||||
deprecated: false,
|
||||
showExpandCollapse: true,
|
||||
expandCollapseThreshold: 100,
|
||||
customInputValue: '',
|
||||
customInputPlaceholder: '',
|
||||
showCustomInput: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'update:customInputValue', value: string): void
|
||||
}>()
|
||||
|
||||
// Track if description is expanded
|
||||
const isExpanded = ref(false)
|
||||
|
||||
// Toggle description expansion
|
||||
function toggleExpansion() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
// Update custom input value
|
||||
function updateCustomInput(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:customInputValue', target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
:key="id"
|
||||
class="relative flex cursor-pointer items-start rounded-lg p-3 pr-[20px] transition-all duration-200 ease-in-out"
|
||||
border="2 solid"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400'
|
||||
: 'bg-white dark:bg-neutral-900/20 border-neutral-200 dark:border-neutral-700 hover:border-primary-500/30 dark:hover:border-primary-400/30',
|
||||
deprecated ? 'opacity-60' : '',
|
||||
]"
|
||||
>
|
||||
<input
|
||||
:checked="modelValue === value"
|
||||
type="radio"
|
||||
:name="name"
|
||||
:value="value"
|
||||
class="absolute opacity-0"
|
||||
@change="$emit('update:modelValue', value)"
|
||||
>
|
||||
<div class="relative mr-3 mt-0.5 flex-shrink-0">
|
||||
<div
|
||||
class="size-5 border-2 rounded-full transition-colors duration-200"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'border-primary-500 dark:border-primary-400'
|
||||
: 'border-neutral-300 dark:border-neutral-600',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 size-3 rounded-full transition-opacity duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'opacity-100 bg-primary-500 dark:bg-primary-400'
|
||||
: 'opacity-0',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="line-clamp-1 font-medium"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'text-neutral-700 dark:text-neutral-300'
|
||||
: 'text-neutral-700 dark:text-neutral-400',
|
||||
]"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Truncated description with expand/collapse functionality -->
|
||||
<div v-if="description" class="relative">
|
||||
<!-- Description with ellipsis (limited to 2 lines) -->
|
||||
<TransitionVertical>
|
||||
<div
|
||||
v-if="!isExpanded"
|
||||
class="line-clamp-2 cursor-pointer text-xs"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'text-neutral-600 dark:text-neutral-400'
|
||||
: 'text-neutral-500 dark:text-neutral-500',
|
||||
]"
|
||||
:title="description"
|
||||
@click.prevent="toggleExpansion"
|
||||
>
|
||||
{{ description }}
|
||||
</div>
|
||||
|
||||
<!-- Expanded description -->
|
||||
<div
|
||||
v-else
|
||||
class="cursor-pointer text-xs"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'text-neutral-600 dark:text-neutral-400'
|
||||
: 'text-neutral-500 dark:text-neutral-500',
|
||||
]"
|
||||
@click.prevent="toggleExpansion"
|
||||
>
|
||||
{{ description }}
|
||||
</div>
|
||||
</TransitionVertical>
|
||||
|
||||
<!-- Expand/collapse button for long descriptions -->
|
||||
<button
|
||||
v-if="showExpandCollapse && description.length > expandCollapseThreshold"
|
||||
class="text-primary-500 dark:text-primary-400 mt-0.5 inline-flex items-center text-xs"
|
||||
@click.prevent="toggleExpansion"
|
||||
>
|
||||
<span>{{ isExpanded ? 'Show less' : 'Show more' }}</span>
|
||||
<div
|
||||
:class="{ 'rotate-180': isExpanded }"
|
||||
class="transition-transform duration-200"
|
||||
>
|
||||
<div i-solar:alt-arrow-down-bold-duotone ml-0.5 text-xs />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom model input field -->
|
||||
<div v-if="showCustomInput && modelValue === value" class="mt-2">
|
||||
<input
|
||||
:value="customInputValue"
|
||||
type="text"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-2 py-1 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||
:placeholder="customInputPlaceholder"
|
||||
@input="updateCustomInput"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
title: string
|
||||
description?: string
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
:key="id"
|
||||
border="2px solid"
|
||||
class="relative transition-all duration-200 ease-in-out"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 dark:border-primary-400'
|
||||
: 'bg-white dark:bg-neutral-900/20 border-neutral-200 dark:border-neutral-700 hover:border-primary-500/30 dark:hover:border-primary-400/30',
|
||||
]"
|
||||
flex="~ col"
|
||||
block min-w-50 w-fit cursor-pointer items-start rounded-lg p-4 text-left
|
||||
>
|
||||
<input
|
||||
:checked="modelValue === value"
|
||||
type="radio"
|
||||
:name="name"
|
||||
:value="value"
|
||||
class="[&:checked+div]:border-primary-500 dark:[&:checked+div]:border-primary-400 absolute opacity-0 [&:checked+div_.radio-dot]:opacity-100"
|
||||
@change="$emit('update:modelValue', value)"
|
||||
>
|
||||
<div
|
||||
class="radio-circle absolute left-2 top-2 size-5 rounded-full"
|
||||
border="2 solid neutral-300 dark:neutral-600"
|
||||
transition="all duration-200 ease-in-out"
|
||||
>
|
||||
<div
|
||||
class="radio-dot absolute left-1/2 top-1/2 size-3 rounded-full opacity-0 -translate-x-1/2 -translate-y-1/2"
|
||||
transition="all duration-200 ease-in-out"
|
||||
bg="primary-500 dark:primary-400"
|
||||
/>
|
||||
</div>
|
||||
<div flex="~ col" min-h-16 w-full items-start justify-center pb-2 pl-5 pr-4 pt-2>
|
||||
<span
|
||||
class="radio-item-name font-bold"
|
||||
text="md"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'text-neutral-700 dark:text-neutral-300'
|
||||
: 'text-neutral-500 dark:text-neutral-500',
|
||||
]"
|
||||
transition="all duration-200 ease-in-out"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span
|
||||
v-if="description"
|
||||
class="radio-item-description"
|
||||
:class="[
|
||||
modelValue === value
|
||||
? 'text-neutral-600 dark:text-neutral-400'
|
||||
: 'text-neutral-400 dark:text-neutral-600',
|
||||
]"
|
||||
transition="all duration-200 ease-in-out"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="bg-dotted-neutral-200/80 dark:bg-dotted-neutral-700/50 [input:checked~&]:bg-dotted-primary-300/50 dark:[input:checked~&]:bg-dotted-primary-200/20"
|
||||
absolute inset-0 z--1
|
||||
style="background-size: 10px 10px; mask-image: linear-gradient(165deg, white 30%, transparent 50%);"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
@@ -1,5 +1,7 @@
|
||||
export { default as BasicTextarea } from './BasicTextarea.vue'
|
||||
export { default as Collapsable } from './Collapsable.vue'
|
||||
export { default as RadioCardDetail } from './Form/Radio/RadioCardDetail.vue'
|
||||
export { default as RadioCardSimple } from './Form/Radio/RadioCardSimple.vue'
|
||||
export { default as Live2DCanvas } from './Live2D/Canvas.vue'
|
||||
export { default as Live2DModel } from './Live2D/Model.vue'
|
||||
export { default as SceneLive2D } from './Scenes/Live2D.vue'
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './chat'
|
||||
export * from './llm'
|
||||
export * from './providers'
|
||||
export * from './settings'
|
||||
export * from './settings/index'
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import {
|
||||
createDeepSeek,
|
||||
createFireworks,
|
||||
createMistral,
|
||||
createMoonshot,
|
||||
createNovita,
|
||||
createOpenAI,
|
||||
createOpenRouter,
|
||||
createTogetherAI,
|
||||
createWorkersAI,
|
||||
createXAI,
|
||||
} from '@xsai-ext/providers-cloud'
|
||||
import { createOllama, createUnElevenLabs } from '@xsai-ext/providers-local'
|
||||
import { listModels } from '@xsai/model'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export interface ProviderMetadata {
|
||||
id: string
|
||||
nameKey: string // i18n key for provider name
|
||||
name: string // Default name (fallback)
|
||||
descriptionKey: string // i18n key for description
|
||||
description: string // Default description (fallback)
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
iconImage?: string
|
||||
baseUrlDefault?: string
|
||||
createProvider?: (config: Record<string, unknown>) => any
|
||||
modelSelectionType: 'dynamic' | 'manual' | 'hardcoded'
|
||||
fetchModelsManually?: (config: Record<string, unknown>) => Promise<ModelInfo[]>
|
||||
hardcodedModels?: ModelInfo[]
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
description?: string
|
||||
capabilities?: string[]
|
||||
contextLength?: number
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export const useProvidersStore = defineStore('providers', () => {
|
||||
const providers = useLocalStorage<Record<string, Record<string, unknown>>>('settings/credentials/providers', {})
|
||||
@@ -8,24 +50,505 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
const coreControllerProvider = useLocalStorage<string>('settings/credentials/coreControllerProvider', 'openai')
|
||||
const audioSynthesisProvider = useLocalStorage<string>('settings/credentials/audioSynthesisProvider', 'elevenlabs')
|
||||
|
||||
const configuredForOpenRouter = computed<boolean>(() => {
|
||||
return !!providers.value['openrouter-ai'].baseUrl && !!providers.value['openrouter-ai'].apiKey
|
||||
})
|
||||
// Helper function to fetch OpenRouter models manually
|
||||
async function fetchOpenRouterModels(config: Record<string, unknown>): Promise<ModelInfo[]> {
|
||||
try {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.apiKey as string}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
function configuredFor(provider: string): boolean {
|
||||
switch (provider) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch OpenRouter models: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.data.map((model: any) => ({
|
||||
id: model.id,
|
||||
name: model.name || model.id,
|
||||
provider: 'openrouter-ai',
|
||||
description: model.description || '',
|
||||
contextLength: model.context_length,
|
||||
deprecated: false,
|
||||
}))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching OpenRouter models:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Centralized provider metadata with provider factory functions
|
||||
const providerMetadata: Record<string, ProviderMetadata> = {
|
||||
'openrouter-ai': {
|
||||
id: 'openrouter-ai',
|
||||
nameKey: 'providers.openrouter.name',
|
||||
name: 'OpenRouter',
|
||||
descriptionKey: 'providers.openrouter.description',
|
||||
description: 'openrouter.ai',
|
||||
icon: 'i-lobe-icons:openrouter',
|
||||
baseUrlDefault: 'https://openrouter.ai/api/v1/',
|
||||
createProvider: config => createOpenRouter(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'manual',
|
||||
fetchModelsManually: fetchOpenRouterModels,
|
||||
},
|
||||
'openai': {
|
||||
id: 'openai',
|
||||
nameKey: 'providers.openai.name',
|
||||
name: 'OpenAI',
|
||||
descriptionKey: 'providers.openai.description',
|
||||
description: 'openai.com',
|
||||
icon: 'i-lobe-icons:openai',
|
||||
baseUrlDefault: 'https://api.openai.com/v1/',
|
||||
createProvider: config => createOpenAI(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'ollama-ai': {
|
||||
id: 'ollama-ai',
|
||||
nameKey: 'providers.ollama.name',
|
||||
name: 'Ollama',
|
||||
descriptionKey: 'providers.ollama.description',
|
||||
description: 'ollama.com',
|
||||
icon: 'i-lobe-icons:ollama',
|
||||
baseUrlDefault: 'http://localhost:11434/api/',
|
||||
createProvider: config => createOllama(config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'vllm': {
|
||||
id: 'vllm',
|
||||
nameKey: 'providers.vllm.name',
|
||||
name: 'vLLM',
|
||||
descriptionKey: 'providers.vllm.description',
|
||||
description: 'vllm.ai',
|
||||
iconColor: 'i-lobe-icons:vllm-color',
|
||||
createProvider: config => createOllama(config.baseUrl as string),
|
||||
modelSelectionType: 'hardcoded',
|
||||
hardcodedModels: [
|
||||
{
|
||||
id: 'llama-2-7b',
|
||||
name: 'Llama 2 (7B)',
|
||||
provider: 'vllm',
|
||||
description: 'Meta\'s Llama 2 7B parameter model',
|
||||
contextLength: 4096,
|
||||
},
|
||||
{
|
||||
id: 'llama-2-13b',
|
||||
name: 'Llama 2 (13B)',
|
||||
provider: 'vllm',
|
||||
description: 'Meta\'s Llama 2 13B parameter model',
|
||||
contextLength: 4096,
|
||||
},
|
||||
{
|
||||
id: 'llama-2-70b',
|
||||
name: 'Llama 2 (70B)',
|
||||
provider: 'vllm',
|
||||
description: 'Meta\'s Llama 2 70B parameter model',
|
||||
contextLength: 4096,
|
||||
},
|
||||
{
|
||||
id: 'mistral-7b',
|
||||
name: 'Mistral (7B)',
|
||||
provider: 'vllm',
|
||||
description: 'Mistral AI\'s 7B parameter model',
|
||||
contextLength: 8192,
|
||||
},
|
||||
{
|
||||
id: 'mixtral-8x7b',
|
||||
name: 'Mixtral (8x7B)',
|
||||
provider: 'vllm',
|
||||
description: 'Mistral AI\'s Mixtral 8x7B MoE model',
|
||||
contextLength: 32768,
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
name: 'Custom Model',
|
||||
provider: 'vllm',
|
||||
description: 'Specify a custom model name',
|
||||
contextLength: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
'perplexity-ai': {
|
||||
id: 'perplexity-ai',
|
||||
nameKey: 'providers.perplexity.name',
|
||||
name: 'Perplexity',
|
||||
descriptionKey: 'providers.perplexity.description',
|
||||
description: 'perplexity.ai',
|
||||
icon: 'i-lobe-icons:perplexity',
|
||||
baseUrlDefault: 'https://api.perplexity.ai',
|
||||
createProvider: config => ({
|
||||
apiKey: config.apiKey as string,
|
||||
baseUrl: config.baseUrl as string,
|
||||
}),
|
||||
modelSelectionType: 'hardcoded',
|
||||
hardcodedModels: [
|
||||
{
|
||||
id: 'sonar-small-online',
|
||||
name: 'Sonar Small (Online)',
|
||||
provider: 'perplexity-ai',
|
||||
description: 'Efficient model with online search capabilities',
|
||||
contextLength: 12000,
|
||||
},
|
||||
{
|
||||
id: 'sonar-medium-online',
|
||||
name: 'Sonar Medium (Online)',
|
||||
provider: 'perplexity-ai',
|
||||
description: 'Balanced model with online search capabilities',
|
||||
contextLength: 12000,
|
||||
},
|
||||
{
|
||||
id: 'sonar-large-online',
|
||||
name: 'Sonar Large (Online)',
|
||||
provider: 'perplexity-ai',
|
||||
description: 'Powerful model with online search capabilities',
|
||||
contextLength: 12000,
|
||||
},
|
||||
{
|
||||
id: 'codey-small',
|
||||
name: 'Codey Small',
|
||||
provider: 'perplexity-ai',
|
||||
description: 'Specialized for code generation and understanding',
|
||||
contextLength: 12000,
|
||||
},
|
||||
{
|
||||
id: 'codey-large',
|
||||
name: 'Codey Large',
|
||||
provider: 'perplexity-ai',
|
||||
description: 'Advanced code generation and understanding',
|
||||
contextLength: 12000,
|
||||
},
|
||||
],
|
||||
},
|
||||
'elevenlabs': {
|
||||
id: 'elevenlabs',
|
||||
nameKey: 'providers.elevenlabs.name',
|
||||
name: 'ElevenLabs',
|
||||
descriptionKey: 'providers.elevenlabs.description',
|
||||
description: 'elevenlabs.io',
|
||||
icon: 'i-simple-icons:elevenlabs',
|
||||
baseUrlDefault: 'https://unspeech.hyp3r.link/v1/',
|
||||
createProvider: config => createUnElevenLabs(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'hardcoded',
|
||||
hardcodedModels: [
|
||||
{
|
||||
id: 'sonar-small-online',
|
||||
name: 'Sonar Small (Online)',
|
||||
provider: 'perplexity-ai',
|
||||
description: 'Efficient model with online search capabilities',
|
||||
contextLength: 12000,
|
||||
},
|
||||
],
|
||||
},
|
||||
'xai': {
|
||||
id: 'xai',
|
||||
nameKey: 'providers.xai.name',
|
||||
name: 'xAI',
|
||||
descriptionKey: 'providers.xai.description',
|
||||
description: 'x.ai',
|
||||
icon: 'i-lobe-icons:xai',
|
||||
createProvider: config => createXAI(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'deepseek': {
|
||||
id: 'deepseek',
|
||||
nameKey: 'providers.deepseek.name',
|
||||
name: 'DeepSeek',
|
||||
descriptionKey: 'providers.deepseek.description',
|
||||
description: 'deepseek.com',
|
||||
iconColor: 'i-lobe-icons:deepseek-color',
|
||||
createProvider: config => createDeepSeek(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'together-ai': {
|
||||
id: 'together-ai',
|
||||
nameKey: 'providers.together.name',
|
||||
name: 'Together.ai',
|
||||
descriptionKey: 'providers.together.description',
|
||||
description: 'together.ai',
|
||||
iconColor: 'i-lobe-icons:together-color',
|
||||
createProvider: config => createTogetherAI(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'novita-ai': {
|
||||
id: 'novita-ai',
|
||||
nameKey: 'providers.novita.name',
|
||||
name: 'Novita',
|
||||
descriptionKey: 'providers.novita.description',
|
||||
description: 'novita.ai',
|
||||
iconColor: 'i-lobe-icons:novita-color',
|
||||
createProvider: config => createNovita(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'fireworks-ai': {
|
||||
id: 'fireworks-ai',
|
||||
nameKey: 'providers.fireworks.name',
|
||||
name: 'Fireworks.ai',
|
||||
descriptionKey: 'providers.fireworks.description',
|
||||
description: 'fireworks.ai',
|
||||
icon: 'i-lobe-icons:fireworks',
|
||||
createProvider: config => createFireworks(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'cloudflare-workers-ai': {
|
||||
id: 'cloudflare-workers-ai',
|
||||
nameKey: 'providers.cloudflare.name',
|
||||
name: 'Cloudflare Workers AI',
|
||||
descriptionKey: 'providers.cloudflare.description',
|
||||
description: 'cloudflare.com',
|
||||
iconColor: 'i-lobe-icons:cloudflare-color',
|
||||
createProvider: config => createWorkersAI(config.apiKey as string, config.accountId as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'mistral-ai': {
|
||||
id: 'mistral-ai',
|
||||
nameKey: 'providers.mistral.name',
|
||||
name: 'Mistral',
|
||||
descriptionKey: 'providers.mistral.description',
|
||||
description: 'mistral.ai',
|
||||
iconColor: 'i-lobe-icons:mistral-color',
|
||||
createProvider: config => createMistral(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
'moonshot-ai': {
|
||||
id: 'moonshot-ai',
|
||||
nameKey: 'providers.moonshot.name',
|
||||
name: 'Moonshot AI',
|
||||
descriptionKey: 'providers.moonshot.description',
|
||||
description: 'moonshot.ai',
|
||||
icon: 'i-lobe-icons:moonshot',
|
||||
createProvider: config => createMoonshot(config.apiKey as string, config.baseUrl as string),
|
||||
modelSelectionType: 'dynamic',
|
||||
},
|
||||
}
|
||||
|
||||
// Configuration validation functions
|
||||
function validateProvider(providerId: string): boolean {
|
||||
const config = providers.value[providerId]
|
||||
if (!config)
|
||||
return false
|
||||
|
||||
switch (providerId) {
|
||||
case 'openrouter-ai':
|
||||
return configuredForOpenRouter.value
|
||||
return !!config.apiKey && !!config.baseUrl
|
||||
case 'openai':
|
||||
return !!config.apiKey
|
||||
case 'ollama-ai':
|
||||
return !!config.baseUrl
|
||||
case 'vllm':
|
||||
return !!config.baseUrl
|
||||
case 'elevenlabs':
|
||||
return !!config.apiKey
|
||||
case 'xai':
|
||||
return !!config.apiKey
|
||||
case 'deepseek':
|
||||
return !!config.apiKey
|
||||
case 'together-ai':
|
||||
return !!config.apiKey
|
||||
case 'novita-ai':
|
||||
return !!config.apiKey
|
||||
case 'fireworks-ai':
|
||||
return !!config.apiKey
|
||||
case 'cloudflare-workers-ai':
|
||||
return !!config.apiKey
|
||||
case 'mistral-ai':
|
||||
return !!config.apiKey
|
||||
case 'moonshot-ai':
|
||||
return !!config.apiKey
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Create computed properties for each provider's configuration status
|
||||
const configuredProviders = ref<Record<string, boolean>>({})
|
||||
|
||||
// Initialize provider configurations
|
||||
function initializeProvider(providerId: string) {
|
||||
if (!providers.value[providerId]) {
|
||||
const metadata = providerMetadata[providerId]
|
||||
providers.value[providerId] = {
|
||||
baseUrl: metadata.baseUrlDefault || '',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all providers
|
||||
Object.keys(providerMetadata).forEach(initializeProvider)
|
||||
|
||||
// Update configuration status for all providers
|
||||
function updateConfigurationStatus() {
|
||||
Object.keys(providerMetadata).forEach((providerId) => {
|
||||
configuredProviders.value[providerId] = validateProvider(providerId)
|
||||
})
|
||||
}
|
||||
|
||||
// Call initially and watch for changes
|
||||
updateConfigurationStatus()
|
||||
watch(providers, updateConfigurationStatus, { deep: true })
|
||||
|
||||
// Available providers (only those that are properly configured)
|
||||
const availableProviders = computed(() => {
|
||||
return Object.keys(providerMetadata).filter(providerId =>
|
||||
configuredProviders.value[providerId],
|
||||
)
|
||||
})
|
||||
|
||||
// Store available models for each provider
|
||||
const availableModels = ref<Record<string, ModelInfo[]>>({})
|
||||
const isLoadingModels = ref<Record<string, boolean>>({})
|
||||
const modelLoadError = ref<Record<string, string | null>>({})
|
||||
|
||||
// Check if a provider supports model listing (any type)
|
||||
function supportsModelListing(providerId: string): boolean {
|
||||
const metadata = providerMetadata[providerId]
|
||||
if (!metadata)
|
||||
return false
|
||||
|
||||
return metadata.modelSelectionType === 'dynamic'
|
||||
|| metadata.modelSelectionType === 'manual'
|
||||
|| (metadata.modelSelectionType === 'hardcoded' && !!metadata.hardcodedModels?.length)
|
||||
}
|
||||
|
||||
// Function to fetch models for a specific provider
|
||||
async function fetchModelsForProvider(providerId: string) {
|
||||
const config = providers.value[providerId]
|
||||
if (!config)
|
||||
return []
|
||||
|
||||
const metadata = providerMetadata[providerId]
|
||||
if (!metadata)
|
||||
return []
|
||||
|
||||
isLoadingModels.value[providerId] = true
|
||||
modelLoadError.value[providerId] = null
|
||||
|
||||
try {
|
||||
// Handle different model selection types
|
||||
switch (metadata.modelSelectionType) {
|
||||
case 'dynamic':
|
||||
if (!metadata.createProvider) {
|
||||
throw new Error(`Provider ${providerId} has no createProvider function`)
|
||||
}
|
||||
|
||||
const providerInstance = metadata.createProvider(config)
|
||||
|
||||
// Check if provider supports model listing
|
||||
if (!('model' in providerInstance && typeof providerInstance.model === 'function')) {
|
||||
throw new Error(`Provider ${providerId} does not support model listing`)
|
||||
}
|
||||
|
||||
// Get models using the provider's model() function
|
||||
const models = await listModels(providerInstance.model())
|
||||
|
||||
// Transform and store the models
|
||||
availableModels.value[providerId] = models.map(model => ({
|
||||
id: model.id,
|
||||
name: model.id,
|
||||
provider: providerId,
|
||||
}))
|
||||
break
|
||||
|
||||
case 'manual':
|
||||
if (!metadata.fetchModelsManually) {
|
||||
throw new Error(`Provider ${providerId} has no fetchModelsManually function`)
|
||||
}
|
||||
|
||||
// Use custom fetch function
|
||||
availableModels.value[providerId] = await metadata.fetchModelsManually(config)
|
||||
break
|
||||
|
||||
case 'hardcoded':
|
||||
if (!metadata.hardcodedModels) {
|
||||
throw new Error(`Provider ${providerId} has no hardcodedModels defined`)
|
||||
}
|
||||
|
||||
// Use hardcoded models
|
||||
availableModels.value[providerId] = metadata.hardcodedModels
|
||||
break
|
||||
}
|
||||
|
||||
return availableModels.value[providerId]
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error fetching models for ${providerId}:`, error)
|
||||
modelLoadError.value[providerId] = error instanceof Error ? error.message : 'Unknown error'
|
||||
return []
|
||||
}
|
||||
finally {
|
||||
isLoadingModels.value[providerId] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Get models for a specific provider
|
||||
function getModelsForProvider(providerId: string) {
|
||||
return availableModels.value[providerId] || []
|
||||
}
|
||||
|
||||
// Get all available models across all configured providers
|
||||
const allAvailableModels = computed(() => {
|
||||
const models: ModelInfo[] = []
|
||||
for (const providerId of availableProviders.value) {
|
||||
models.push(...(availableModels.value[providerId] || []))
|
||||
}
|
||||
return models
|
||||
})
|
||||
|
||||
// Load models for all configured providers
|
||||
async function loadModelsForConfiguredProviders() {
|
||||
for (const providerId of availableProviders.value) {
|
||||
if (supportsModelListing(providerId)) {
|
||||
await fetchModelsForProvider(providerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get localized provider metadata
|
||||
function getProviderMetadata(providerId: string) {
|
||||
const { t } = useI18n()
|
||||
const metadata = providerMetadata[providerId]
|
||||
|
||||
if (!metadata)
|
||||
throw new Error(`Provider metadata for ${providerId} not found`)
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
localizedName: t(metadata.nameKey, metadata.name),
|
||||
localizedDescription: t(metadata.descriptionKey, metadata.description),
|
||||
}
|
||||
}
|
||||
|
||||
// Get all providers metadata (for settings page)
|
||||
const allProvidersMetadata = computed(() => {
|
||||
const { t } = useI18n()
|
||||
|
||||
return Object.values(providerMetadata).map(metadata => ({
|
||||
...metadata,
|
||||
localizedName: t(metadata.nameKey, metadata.name),
|
||||
localizedDescription: t(metadata.descriptionKey, metadata.description),
|
||||
configured: configuredProviders.value[metadata.id] || false,
|
||||
}))
|
||||
})
|
||||
|
||||
return {
|
||||
providers,
|
||||
coreControllerProvider,
|
||||
audioSynthesisProvider,
|
||||
configuredForOpenRouter,
|
||||
configuredFor,
|
||||
availableProviders,
|
||||
configuredProviders,
|
||||
providerMetadata,
|
||||
getProviderMetadata,
|
||||
allProvidersMetadata,
|
||||
initializeProvider,
|
||||
validateProvider,
|
||||
availableModels,
|
||||
isLoadingModels,
|
||||
modelLoadError,
|
||||
fetchModelsForProvider,
|
||||
getModelsForProvider,
|
||||
allAvailableModels,
|
||||
loadModelsForConfiguredProviders,
|
||||
supportsModelListing,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './modules'
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useProvidersStore } from '../../providers'
|
||||
|
||||
export const useConsciousnessStore = defineStore('consciousness', () => {
|
||||
const providersStore = useProvidersStore()
|
||||
|
||||
// State
|
||||
const activeProvider = useLocalStorage('settings/consciousness/active-provider', '')
|
||||
const activeModel = useLocalStorage('settings/consciousness/active-model', '')
|
||||
const activeCustomModelName = useLocalStorage('settings/consciousness/active-custom-model', '')
|
||||
const expandedDescriptions = ref<Record<string, boolean>>({})
|
||||
const modelSearchQuery = ref('')
|
||||
|
||||
// Computed properties
|
||||
const availableProvidersMetadata = computed(() => {
|
||||
return providersStore.availableProviders.map(id => providersStore.getProviderMetadata(id))
|
||||
})
|
||||
|
||||
const supportsModelListing = computed(() => {
|
||||
return providersStore.supportsModelListing(activeProvider.value)
|
||||
})
|
||||
|
||||
const providerModels = computed(() => {
|
||||
return providersStore.getModelsForProvider(activeProvider.value)
|
||||
})
|
||||
|
||||
const isLoadingActiveProviderModels = computed(() => {
|
||||
return providersStore.isLoadingModels[activeProvider.value] || false
|
||||
})
|
||||
|
||||
const activeProviderModelError = computed(() => {
|
||||
return providersStore.modelLoadError[activeProvider.value] || null
|
||||
})
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
if (!modelSearchQuery.value.trim()) {
|
||||
return providerModels.value
|
||||
}
|
||||
|
||||
const query = modelSearchQuery.value.toLowerCase().trim()
|
||||
return providerModels.value.filter(model =>
|
||||
model.name.toLowerCase().includes(query)
|
||||
|| model.id.toLowerCase().includes(query)
|
||||
|| (model.description && model.description.toLowerCase().includes(query)),
|
||||
)
|
||||
})
|
||||
|
||||
// Actions
|
||||
function setActiveProvider(provider: string) {
|
||||
activeProvider.value = provider
|
||||
}
|
||||
|
||||
function setActiveModel(model: string) {
|
||||
activeModel.value = model
|
||||
}
|
||||
|
||||
function setCustomModelName(name: string) {
|
||||
activeCustomModelName.value = name
|
||||
}
|
||||
|
||||
function setModelSearchQuery(query: string) {
|
||||
modelSearchQuery.value = query
|
||||
}
|
||||
|
||||
function resetModelSelection() {
|
||||
activeModel.value = ''
|
||||
activeCustomModelName.value = ''
|
||||
expandedDescriptions.value = {}
|
||||
modelSearchQuery.value = ''
|
||||
}
|
||||
|
||||
async function loadModelsForProvider(provider: string) {
|
||||
if (provider && providersStore.supportsModelListing(provider)
|
||||
&& providersStore.getModelsForProvider(provider).length === 0) {
|
||||
await providersStore.fetchModelsForProvider(provider)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for provider changes and load models
|
||||
watch(activeProvider, async (newProvider) => {
|
||||
await loadModelsForProvider(newProvider)
|
||||
resetModelSelection()
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
activeProvider,
|
||||
activeModel,
|
||||
customModelName: activeCustomModelName,
|
||||
expandedDescriptions,
|
||||
modelSearchQuery,
|
||||
|
||||
// Computed
|
||||
availableProvidersMetadata,
|
||||
supportsModelListing,
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
filteredModels,
|
||||
|
||||
// Actions
|
||||
setActiveProvider,
|
||||
setActiveModel,
|
||||
setCustomModelName,
|
||||
setModelSearchQuery,
|
||||
resetModelSelection,
|
||||
loadModelsForProvider,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from './consciousness'
|
||||
Generated
+26
-20
@@ -228,6 +228,9 @@ importers:
|
||||
'@vueuse/shared':
|
||||
specifier: ^12.8.2
|
||||
version: 12.8.2(typescript@5.8.2)
|
||||
'@xsai-ext/providers-cloud':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.2
|
||||
'@xsai-ext/providers-local':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.2
|
||||
@@ -345,7 +348,7 @@ importers:
|
||||
version: 2.3.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
'@proj-airi/elevenlabs':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/elevenlabs
|
||||
@@ -372,10 +375,10 @@ importers:
|
||||
version: 5.2.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/volar':
|
||||
specifier: ^3.0.0-beta.3
|
||||
version: 3.0.0-beta.3(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 3.0.0-beta.3(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/motion':
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6(magicast@0.3.5)(rollup@4.34.9)(vue@3.5.13(typescript@5.8.2))
|
||||
version: 2.2.6(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2))
|
||||
electron:
|
||||
specifier: ^34.3.1
|
||||
version: 34.3.1
|
||||
@@ -393,13 +396,13 @@ importers:
|
||||
version: 3.2.0(unocss@66.1.0-beta.3(postcss@8.5.3)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)))
|
||||
unplugin-auto-import:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(@vueuse/core@12.8.2(typescript@5.8.2))
|
||||
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.8.2(typescript@5.8.2))
|
||||
unplugin-vue-components:
|
||||
specifier: ^28.4.1
|
||||
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-macros:
|
||||
specifier: ^2.14.5
|
||||
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-markdown:
|
||||
specifier: ^28.3.1
|
||||
version: 28.3.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
@@ -408,13 +411,13 @@ importers:
|
||||
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rollup@4.34.9)
|
||||
version: 1.2.1(rollup@2.79.1)
|
||||
vite-plugin-pwa:
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^7.7.2
|
||||
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(rollup@4.34.9)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
@@ -517,6 +520,9 @@ importers:
|
||||
'@vueuse/shared':
|
||||
specifier: ^12.8.2
|
||||
version: 12.8.2(typescript@5.8.2)
|
||||
'@xsai-ext/providers-cloud':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.2
|
||||
'@xsai-ext/providers-local':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.2
|
||||
@@ -655,7 +661,7 @@ importers:
|
||||
version: 2.3.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.8.2)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
'@proj-airi/drizzle-duckdb-wasm':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/drizzle-duckdb-wasm
|
||||
@@ -697,10 +703,10 @@ importers:
|
||||
version: 5.2.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/volar':
|
||||
specifier: ^3.0.0-beta.3
|
||||
version: 3.0.0-beta.3(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 3.0.0-beta.3(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/motion':
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2))
|
||||
version: 2.2.6(magicast@0.3.5)(rollup@4.34.9)(vue@3.5.13(typescript@5.8.2))
|
||||
hfup:
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/hfup
|
||||
@@ -712,13 +718,13 @@ importers:
|
||||
version: 4.0.1
|
||||
unplugin-auto-import:
|
||||
specifier: ^19.1.1
|
||||
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.8.2(typescript@5.8.2))
|
||||
version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(@vueuse/core@12.8.2(typescript@5.8.2))
|
||||
unplugin-vue-components:
|
||||
specifier: ^28.4.1
|
||||
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-macros:
|
||||
specifier: ^2.14.5
|
||||
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin-vue-markdown:
|
||||
specifier: ^28.3.1
|
||||
version: 28.3.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
@@ -727,13 +733,13 @@ importers:
|
||||
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rollup@2.79.1)
|
||||
version: 1.2.1(rollup@4.34.9)
|
||||
vite-plugin-pwa:
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^7.7.2
|
||||
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(rollup@4.34.9)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||
@@ -23170,9 +23176,9 @@ snapshots:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.34.9)
|
||||
'@vueuse/core': 12.8.2(typescript@5.8.2)
|
||||
|
||||
unplugin-combine@1.2.0(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
|
||||
unplugin-combine@1.2.0(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.19.12
|
||||
esbuild: 0.25.0
|
||||
rollup: 2.79.1
|
||||
unplugin: 1.16.1
|
||||
vite: 6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
|
||||
@@ -23231,7 +23237,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
unplugin-vue-macros@2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
|
||||
unplugin-vue-macros@2.14.5(@vueuse/core@12.8.2(typescript@5.8.2))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
@@ -23263,7 +23269,7 @@ snapshots:
|
||||
'@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue-macros/volar': 0.30.15(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.8(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2))
|
||||
unplugin: 1.16.1
|
||||
unplugin-combine: 1.2.0(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
unplugin-combine: 1.2.0(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
|
||||
unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.2))
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user