feat(stage-web): module consciousness setting

This commit is contained in:
Neko Ayaka
2025-03-11 01:15:38 +08:00
parent 5529b9cb5d
commit 5760fbc46f
19 changed files with 1446 additions and 196 deletions
+1
View File
@@ -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:",
+62
View File
@@ -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
+62
View File
@@ -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
+1
View File
@@ -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>