feat(stage-ui|stage-tamagotchi|stage-web): ollama, header & values

This commit is contained in:
Neko Ayaka
2025-03-16 21:56:57 +08:00
parent e466a7afe8
commit 7358023bbb
28 changed files with 577 additions and 743 deletions
@@ -0,0 +1,119 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
FieldKeyValues,
ProviderAdvancedSettings,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
// Get provider metadata
const providerId = 'ollama'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
// Initialize headers if not already set
if (!providers.value[providerId]?.headers) {
providers.value[providerId].headers = {}
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
const headers = reactive<{ key: string, value: string }[]>([
{ key: '', value: '' },
])
function addKeyValue(headers: { key: string, value: string }[], key: string, value: string) {
if (!headers)
return
headers.push({ key, value })
}
function removeKeyValue(index: number, headers: { key: string, value: string }[]) {
if (!headers)
return
if (headers.length === 1) {
headers[0].key = ''
headers[0].value = ''
}
else {
headers.splice(index, 1)
}
}
watch(headers, (headers) => {
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
headers.push({ key: '', value: '' })
}
}, {
deep: true,
immediate: true,
})
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
required
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<FieldKeyValues
v-model="headers"
label="HTTP Headers"
description="Add custom HTTP headers"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(headers, key, value)"
@remove="(index) => removeKeyValue(index, headers)"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -1,30 +0,0 @@
<script setup lang="ts">
import { useDark } from '@vueuse/core'
import LogoDark from '../../assets/logo-dark.svg'
import Logo from '../../assets/logo.svg'
const dark = useDark()
</script>
<template>
<header mb-1 mt-2 w-full gap-2>
<div flex="~ 1" w-full items-center justify-center gap-2 px-2 text-nowrap text-lg>
<template v-if="dark">
<img :src="LogoDark" h-8 w-8 class="theme-colored">
</template>
<template v-else>
<img :src="Logo" h-8 w-8 class="theme-colored">
</template>
<div font-cute>
<span>アイリ</span>
</div>
</div>
</header>
</template>
<style scoped>
.theme-colored {
filter: hue-rotate(calc(var(--theme-colors-hue, 0) * 1deg));
}
</style>
@@ -5,16 +5,10 @@ import { BasicTextarea } from '@proj-airi/stage-ui/components'
import { useMicVAD } from '@proj-airi/stage-ui/composables'
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { DrawerContent, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTrigger } from 'vaul-vue'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import MobileChatHistory from '../Widgets/MobileChatHistory.vue'
import MobileSettings from '../Widgets/MobileSettings.vue'
const emit = defineEmits<{
(e: 'settingsOpen', open: boolean): void
}>()
const messageInput = ref('')
const listening = ref(false)
@@ -67,21 +61,6 @@ function handleTranscription(_buffer: Float32Array<ArrayBufferLike>) {
alert('Transcription is not implemented yet')
}
// async function handleAudioInputChange(event: Event) {
// const target = event.target as HTMLSelectElement
// const found = audioInputs.value.find(d => d.deviceId === target.value)
// if (!found) {
// selectedAudioDevice.value = undefined
// return
// }
// selectedAudioDevice.value = found
// }
function handleSettingsOpen(open: boolean) {
emit('settingsOpen', open)
}
watch(isAudioInputOn, async (value) => {
if (value === 'false') {
destroy()
@@ -106,34 +85,14 @@ onMounted(() => {
v-model="messageInput"
:placeholder="t('stage.message')"
border="solid 2 primary-100 dark:primary-400/20"
text="primary-400 hover:primary-600 dark:[#905073] dark:hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600 placeholder:dark:[#905073] placeholder:dark:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
w-full resize-none overflow-y-scroll rounded-l-xl p-2 font-medium outline-none
text="primary-300 hover:primary-500 dark:primary-300/50 dark:hover:primary-500 placeholder:primary-300 placeholder:hover:primary-500 placeholder:dark:primary-300/50 placeholder:dark:hover:primary-500"
bg="primary-100 dark:primary-400/20"
max-h="[10lh]" min-h="[1lh]"
w-full resize-none overflow-y-scroll rounded-xl p-2 font-medium outline-none
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
@submit="handleSend"
/>
</div>
<DrawerRoot should-scale-background @update:open="handleSettingsOpen">
<DrawerTrigger
class="px-4 py-2.5"
border="solid 2 primary-100 dark:primary-400/20"
text="lg primary-400 hover:primary-600 dark:[#905073] dark:hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600 placeholder:dark:[#905073] placeholder:dark:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]" rounded-r-xl
>
<div i-solar:settings-bold-duotone />
</DrawerTrigger>
<DrawerPortal>
<DrawerOverlay class="fixed inset-0 z-50 bg-black/40" />
<DrawerContent
max-h="[75%]"
fixed bottom-0 left-0 right-0 z-50 mt-24 h-full flex flex-col rounded-t-lg bg="[#fffbff] dark:[#1f1a1d]"
>
<div class="flex flex-1 flex-col rounded-t-lg p-5" bg="[#fffbff] dark:[#1f1a1d]" gap-2>
<MobileSettings />
</div>
</DrawerContent>
</DrawerPortal>
</DrawerRoot>
</div>
</div>
</template>
@@ -1,228 +0,0 @@
<script setup lang="ts">
import { useSettings } from '@proj-airi/stage-ui/stores'
import { useDark } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ModelProviderSettings from './ModelProviderSettings.vue'
const { t } = useI18n()
const settings = storeToRefs(useSettings())
const dark = useDark({ disableTransition: false })
const currentView = ref('main')
const slideDirection = ref('forward') // 'forward' | 'backward'
function handleLanguageChange(event: Event) {
const target = event.target as HTMLSelectElement
settings.language.value = target.value
}
function navigateToProviders() {
slideDirection.value = 'forward'
currentView.value = 'providers'
}
function navigateToLive2D() {
slideDirection.value = 'forward'
currentView.value = 'live2d'
}
function navigateBack() {
slideDirection.value = 'backward'
currentView.value = 'main'
}
</script>
<template>
<div text="zinc-500 dark:zinc-400" class="relative">
<Transition :name="slideDirection === 'forward' ? 'slide-forward' : 'slide-backward'">
<!-- Main Settings View -->
<div v-if="currentView === 'main'" key="main">
<h2 text="zinc-800/80 dark:zinc-200/80 xl" mb-4 font-bold>
{{ t('settings.title') }}
</h2>
<div>
<!-- Model Providers Navigation Item -->
<div class="space-y-2">
<div
grid="~ cols-[150px_1fr]"
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
cursor-pointer items-center gap-1.5 rounded-lg px-4 py-3
@click="navigateToProviders"
>
<div text="sm">
<span>{{ t('settings.model-provider.title') }}</span>
</div>
<div flex="~ row" w-full justify-end text="sm">
<div i-solar:alt-arrow-right-bold-duotone />
</div>
</div>
<!-- Language Setting -->
<div
grid="~ cols-[150px_1fr]"
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
items-center gap-1.5 rounded-lg px-4 py-3
>
<div text="sm">
<span>{{ t('settings.language.title') }}</span>
</div>
<div flex="~ row" w-full justify-end>
<select
class="w-32"
bg="transparent"
text="sm right zinc-800 dark:zinc-100"
transition="all ease-in-out duration-250"
outline="none"
@change="handleLanguageChange"
>
<option value="en-US">
{{ t('settings.language.english') }}
</option>
<option value="zh-CN">
{{ t('settings.language.chinese') }}
</option>
</select>
</div>
</div>
<!-- Theme Setting -->
<label
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
w-full flex cursor-pointer rounded-lg px-4 py-3
>
<input
v-model="dark"
text="zinc-800 dark:zinc-100"
:checked="dark"
:aria-checked="dark"
type="checkbox"
hidden appearance-none outline-none
>
<div flex="~ row" w-full items-center gap-1.5>
<div text="sm" w-full flex-1>
<span>{{ t('settings.theme') }}</span>
</div>
<div select-none>
<Transition name="slide-away" mode="out-in">
<div
v-if="dark"
i-solar:moon-stars-bold-duotone
transition="all ease-in-out duration-250"
/>
<div
v-else
i-solar:sun-fog-bold-duotone
transition="all ease-in-out duration-250"
/>
</Transition>
</div>
</div>
</label>
<!-- Live2D Setting -->
<div
grid="~ cols-[150px_1fr]"
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
cursor-pointer items-center gap-1.5 rounded-lg px-4 py-3
@click="navigateToLive2D"
>
<div text="sm">
<span>{{ t('settings.live2d.title') }}</span>
</div>
<div flex="~ row" w-full justify-end>
<div i-solar:alt-arrow-right-bold-duotone />
</div>
</div>
</div>
</div>
</div>
<!-- Model Providers View -->
<div v-else-if="currentView === 'providers'" key="providers">
<div mb-4 flex items-center gap-2>
<button
text="zinc-800/80 dark:zinc-200/80"
@click="navigateBack"
>
<div i-solar:alt-arrow-left-bold-duotone />
</button>
<h2 text="zinc-800/80 dark:zinc-200/80 xl" font-bold>
{{ t('settings.model-provider.title') }}
</h2>
</div>
<ModelProviderSettings />
</div>
<!-- Live2D Settings View -->
<div v-else-if="currentView === 'live2d'" key="live2d">
<div mb-4 flex items-center gap-2>
<button
text="zinc-800/80 dark:zinc-200/80"
@click="navigateBack"
>
<div i-solar:alt-arrow-left-bold-duotone />
</button>
<h2 text="zinc-800/80 dark:zinc-200/80 xl" font-bold>
{{ t('settings.live2d.title') }}
</h2>
</div>
<Live2DSettings />
</div>
</Transition>
</div>
</template>
<style scoped>
/* Forward navigation transitions */
.slide-forward-enter-active,
.slide-forward-leave-active {
transition: all 0.2s ease;
position: absolute;
width: 100%;
}
.slide-forward-enter-from {
transform: translateX(100%);
}
.slide-forward-leave-to {
transform: translateX(-30%);
opacity: 0;
}
.slide-forward-enter-to,
.slide-forward-leave-from {
transform: translateX(0);
}
/* Backward navigation transitions */
.slide-backward-enter-active,
.slide-backward-leave-active {
transition: all 0.2s ease;
position: absolute;
width: 100%;
}
.slide-backward-enter-from {
transform: translateX(-30%);
opacity: 0;
}
.slide-backward-leave-to {
transform: translateX(100%);
}
.slide-backward-enter-to,
.slide-backward-leave-from {
transform: translateX(0);
}
</style>
@@ -1,381 +0,0 @@
<script setup lang="ts">
import { Collapsable } from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { toJsonSchema } from '@valibot/to-json-schema'
import { storeToRefs } from 'pinia'
import { description, object, optional, pipe, record, string, title } from 'valibot'
import { computed } from 'vue'
interface ModelProvider {
id: string
name: string
icon?: string
fields: ReturnType<typeof toJsonSchema>
}
const providers = computed<ModelProvider[]>(() => [
{
id: 'openrouter-ai',
name: 'OpenRouter',
icon: 'i-lobe-icons:openrouter',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for OpenRouter')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://openrouter.ai/api/v1/'),
}),
),
},
{
id: 'openai',
name: 'OpenAI',
icon: 'i-lobe-icons:openai',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for OpenAI services')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.openai.com/v1/'),
}),
),
},
{
id: 'ollama-ai',
name: 'Ollama',
icon: 'i-lobe-icons:ollama',
fields: toJsonSchema(
object({
baseUrl: optional(pipe(string(), title('Host'), description('Host of the Ollama instance (optional)'))),
extraHeaders: optional(pipe(record(string(), string()), title('Headers'), description('Custom Headers for Ollama instance (optional)'))),
}),
),
},
{
id: 'vllm',
name: 'vLLM',
icon: 'i-lobe-icons:vllm-color',
fields: toJsonSchema(
object({
baseUrl: optional(pipe(string(), title('Host'), description('Host of the vLLM instance (optional)'))),
apiKey: optional(pipe(string(), title('API Key'), description('API Key for vLLM'))),
extraHeaders: optional(pipe(record(string(), string()), title('Headers'), description('Custom Headers for vLLM instance (optional)'))),
}),
),
},
{
id: 'elevenlabs',
name: 'ElevenLabs',
icon: 'i-simple-icons:elevenlabs',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for ElevenLabs')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)'))),
}),
),
},
{
id: 'xai',
name: 'xAI',
icon: 'i-lobe-icons:xai',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for xAI')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.x.ai/v1/'),
}),
),
},
{
id: 'deepseek',
name: 'DeepSeek',
icon: 'i-lobe-icons:deepseek-color',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for DeepSeek')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.deepseek.com/'),
}),
),
},
{
id: 'together-ai',
name: 'Together.ai',
icon: 'i-lobe-icons:together-color',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for Together.ai')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.together.xyz/v1/'),
}),
),
},
{
id: 'novita-ai',
name: 'Novita',
icon: 'i-lobe-icons:novita-color',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for Novita')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.novita.ai/v3/openai/'),
}),
),
},
{
id: 'fireworks-ai',
name: 'Fireworks.ai',
icon: 'i-lobe-icons:fireworks',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for Fireworks.ai')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.fireworks.ai/inference/v1/'),
}),
),
},
{
id: 'cloudflare-workers-ai',
name: 'Cloudflare Workers AI',
icon: 'i-lobe-icons:cloudflare-color',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key')),
accountId: pipe(string(), title('Account ID'), description('Cloudflare Account ID')),
}),
),
},
{
id: 'mistral-ai',
name: 'Mistral',
icon: 'i-lobe-icons:mistral-color',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for OpenRouter')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.mistral.ai/v1/'),
}),
),
},
{
id: 'moonshot-ai',
name: 'Moonshot AI',
icon: 'i-lobe-icons:moonshot',
fields: toJsonSchema(
object({
apiKey: pipe(string(), title('API Key'), description('API Key for OpenRouter')),
baseUrl: optional(pipe(string(), title('Base URL'), description('Custom base URL (optional)')), 'https://api.moonshot.cn/v1/'),
}),
),
},
])
const { providers: providerValues } = storeToRefs(useProvidersStore())
function getFieldValue(providerId: string, fieldName: string): unknown {
return providerValues.value[providerId]?.[fieldName] || ''
}
function setFieldValue(providerId: string, fieldName: string, value: unknown) {
if (!providerValues.value[providerId]) {
providerValues.value[providerId] = {}
}
providerValues.value[providerId][fieldName] = value
}
function getRecordEntries(providerId: string, fieldName: string): Array<[string, string]> {
const value = providerValues.value[providerId]?.[fieldName]
if (!value)
return [['', '']]
try {
return Object.entries(value)
}
catch {
return [['', '']]
}
}
function setRecordValue(providerId: string, fieldName: string, entries: Array<[string, string]>) {
const validEntries = entries.filter(([key, value]) => key || value)
if (validEntries.length === 0) {
delete providerValues.value[providerId]?.[fieldName]
return
}
const record = Object.fromEntries(validEntries)
setFieldValue(providerId, fieldName, record)
}
function addRecordEntry(entries: Array<[string, string]>) {
entries.push(['', ''])
}
function removeRecordEntry(entries: Array<[string, string]>, index: number) {
entries.splice(index, 1)
}
// Helper function to get default value from field schema
function getDefaultValue(field: any, _fieldName: string) {
// Check if field has a default value defined
if (field.default !== undefined) {
return field.default
}
// For record types, return an empty array (for the key-value pairs)
if (field.type === 'object' && field.additionalProperties) {
return []
}
// Return appropriate empty values based on field type
switch (field.type) {
case 'string':
return ''
case 'number':
case 'integer':
return 0
case 'boolean':
return false
case 'array':
return []
case 'object':
return {}
default:
return ''
}
}
// Initialize field with default value if not already set
function initFieldWithDefault(providerId: string, fieldName: string, field: any) {
const currentValue = providerValues.value[providerId]?.[fieldName]
// Only set default if field doesn't already have a value
if (currentValue === undefined) {
const defaultValue = getDefaultValue(field, fieldName)
setFieldValue(providerId, fieldName, defaultValue)
}
}
// Initialize all fields with defaults when provider is expanded
function initProviderDefaults(providerId: string) {
const provider = providers.value.find(p => p.id === providerId)
if (!provider || !provider.fields.properties)
return
Object.entries(provider.fields.properties).forEach(([fieldName, field]) => {
initFieldWithDefault(providerId, fieldName, field)
})
}
</script>
<template>
<div flex="~ col" gap-2>
<div v-for="provider in providers" :key="provider.id">
<Collapsable w-full>
<template #trigger="slotProps">
<button
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="slotProps.setVisible(!slotProps.visible) && initProviderDefaults(provider.id)"
>
<div flex="~ row 1" items-center gap-1.5>
<div
:class="provider.icon" class="provider-icon size-6"
transition="filter duration-250 ease-in-out"
/>
<div>
{{ provider.name }}
</div>
</div>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div p-4>
<div class="space-y-4">
<!-- Dynamically render fields based on JSON Schema -->
<div
v-for="(field, fieldName) in provider.fields.properties"
:key="fieldName"
class="space-y-1"
>
<template v-if="typeof field !== 'boolean' && typeof fieldName !== 'number' && field.type === 'object' && field.additionalProperties">
<!-- Record field (key-value pairs) -->
<div>
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ field.title || fieldName }}
<span v-if="provider.fields.required?.includes(fieldName)" class="text-red-500">*</span>
</div>
<div v-if="field.description" class="text-xs text-zinc-500 dark:text-zinc-400">
{{ field.description }}
</div>
</div>
<button
class="text-sm"
@click="addRecordEntry(getRecordEntries(provider.id, fieldName))"
>
<div i-solar:add-circle-line-duotone />
</button>
</div>
<div class="mt-2 space-y-2">
<div
v-for="(_, index) in getRecordEntries(provider.id, fieldName)"
:key="index"
class="flex items-center gap-2"
>
<input
v-model="getRecordEntries(provider.id, fieldName)[index][0]"
type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
flex-1 rounded px-2 py-1 text-sm outline-none
placeholder="Key"
@input="setRecordValue(provider.id, fieldName, getRecordEntries(provider.id, fieldName))"
>
<input
v-model="getRecordEntries(provider.id, fieldName)[index][1]"
type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
flex-1 rounded px-2 py-1 text-sm outline-none
placeholder="Value"
@input="setRecordValue(provider.id, fieldName, getRecordEntries(provider.id, fieldName))"
>
<button
class="text-red-500 hover:text-red-600"
@click="removeRecordEntry(getRecordEntries(provider.id, fieldName), index)"
>
<div i-solar:trash-bin-trash-bold-duotone />
</button>
</div>
</div>
</div>
</template>
<template v-if="typeof field !== 'boolean' && typeof fieldName !== 'number' && field.type === 'string'">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ field.title }}
<span
v-if="provider.fields.required?.includes(fieldName)"
class="text-red-500"
>*</span>
</div>
<div v-if="field.description" class="text-xs text-zinc-400 dark:text-zinc-600">
{{ field.description }}
</div>
</div>
<input
:type="fieldName.toLowerCase().includes('key') ? 'password' : 'text'"
:value="getFieldValue(provider.id, fieldName)"
rounded
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
px-2 py-1 text-sm outline-none
:placeholder="(field.default && String(field.default)) || `Enter ${field.title || fieldName}`"
@input="(e) => setFieldValue(provider.id, fieldName, (e.target as HTMLInputElement).value)"
>
</div>
</template>
</div>
</div>
</div>
</Collapsable>
</div>
</div>
</template>
+1 -3
View File
@@ -6,7 +6,6 @@ import { ref } from 'vue'
import Cross from '../components/Backgrounds/Cross.vue'
import Header from '../components/Layouts/Header.vue'
import InteractiveArea from '../components/Layouts/InteractiveArea.vue'
import MobileHeader from '../components/Layouts/MobileHeader.vue'
import MobileInteractiveArea from '../components/Layouts/MobileInteractiveArea.vue'
import AnimatedWave from '../components/Widgets/AnimatedWave.vue'
@@ -28,8 +27,7 @@ function handleSettingsOpen(open: boolean) {
<div relative flex="~ col" z-2 h-100vh w-100vw of-hidden>
<!-- header -->
<div>
<Header class="flex <md:hidden" p2 />
<MobileHeader class="hidden <md:block" />
<Header class="flex" p2 />
</div>
<!-- page -->
<div relative flex="~ 1 row gap-y-0 gap-x-2 <md:col">
@@ -0,0 +1,119 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
FieldKeyValues,
ProviderAdvancedSettings,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
// Get provider metadata
const providerId = 'ollama'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
// Initialize headers if not already set
if (!providers.value[providerId]?.headers) {
providers.value[providerId].headers = {}
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
const headers = reactive<{ key: string, value: string }[]>([
{ key: '', value: '' },
])
function addKeyValue(headers: { key: string, value: string }[], key: string, value: string) {
if (!headers)
return
headers.push({ key, value })
}
function removeKeyValue(index: number, headers: { key: string, value: string }[]) {
if (!headers)
return
if (headers.length === 1) {
headers[0].key = ''
headers[0].value = ''
}
else {
headers.splice(index, 1)
}
}
watch(headers, (headers) => {
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
headers.push({ key: '', value: '' })
}
}, {
deep: true,
immediate: true,
})
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
required
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<FieldKeyValues
v-model="headers"
label="HTTP Headers"
description="Add custom HTTP headers"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(headers, key, value)"
@remove="(index) => removeKeyValue(index, headers)"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
+3 -1
View File
@@ -56,7 +56,9 @@
"story:preview": "histoire preview"
},
"dependencies": {
"reka-ui": "^2.1.0"
"@vueuse/motion": "^3.0.3",
"reka-ui": "^2.1.0",
"vue": "link:@formkit/auto-animate/vue"
},
"devDependencies": {
"@electron-toolkit/preload": "^3.0.1",
@@ -22,7 +22,7 @@ const interactiveValue = ref(false)
id="checked"
title="Checked"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Checkbox v-model="checkedValue" />
</div>
</Variant>
@@ -31,7 +31,7 @@ const interactiveValue = ref(false)
id="unchecked"
title="Unchecked"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Checkbox v-model="uncheckedValue" />
</div>
</Variant>
@@ -40,7 +40,7 @@ const interactiveValue = ref(false)
id="interactive"
title="Interactive"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<div class="flex items-center gap-4">
<Checkbox v-model="interactiveValue" />
<div class="text-sm">
@@ -54,7 +54,7 @@ const interactiveValue = ref(false)
id="sizes"
title="Different Contexts"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<Checkbox v-model="checkedValue" />
@@ -21,7 +21,7 @@ const uncheckedValue = ref(false)
id="checked"
title="Checked"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldCheckbox
v-model="checkedValue"
label="Speaker Boost"
@@ -34,7 +34,7 @@ const uncheckedValue = ref(false)
id="unchecked"
title="Unchecked"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldCheckbox
v-model="uncheckedValue"
label="Use High Quality"
@@ -47,7 +47,7 @@ const uncheckedValue = ref(false)
id="interactive"
title="Interactive"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldCheckbox
v-model="checkedValue"
label="Toggle Me"
@@ -22,7 +22,7 @@ const emailValue = ref('user@example.com')
id="text-empty"
title="Text Input (Empty)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderInput
v-model="textValue"
label="Text Input"
@@ -36,7 +36,7 @@ const emailValue = ref('user@example.com')
id="password"
title="Password Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderInput
v-model="passwordValue"
label="Password"
@@ -51,7 +51,7 @@ const emailValue = ref('user@example.com')
id="email"
title="Email Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderInput
v-model="emailValue"
label="Email Address"
@@ -66,7 +66,7 @@ const emailValue = ref('user@example.com')
id="required"
title="Required Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderInput
v-model="textValue"
label="Required Field"
@@ -81,7 +81,7 @@ const emailValue = ref('user@example.com')
id="not-required"
title="Optional Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderInput
v-model="textValue"
label="Optional Field"
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
import FieldKeyValues from './FieldKeyValues.vue'
const emptyHeaders = reactive<{ key: string, value: string }[]>([
{ key: '', value: '' },
])
const singleHeader = reactive<{ key: string, value: string }[]>([
{ key: 'Content-Type', value: 'application/json' },
])
const multipleHeaders = reactive<{ key: string, value: string }[]>([
{ key: 'Authorization', value: 'Bearer token123' },
{ key: 'Accept', value: 'application/json' },
{ key: 'X-API-Key', value: 'abc123xyz456' },
])
function addKeyValue(headers: { key: string, value: string }[], key: string, value: string) {
if (!headers)
return
headers.push({ key, value })
}
function removeKeyValue(index: number, headers: { key: string, value: string }[]) {
if (!headers)
return
if (headers.length === 1) {
headers[0].key = ''
headers[0].value = ''
}
else {
headers.splice(index, 1)
}
}
watch(emptyHeaders, (headers) => {
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
emptyHeaders.push({ key: '', value: '' })
}
}, {
deep: true,
immediate: true,
})
watch(singleHeader, (headers) => {
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
singleHeader.push({ key: '', value: '' })
}
}, {
deep: true,
immediate: true,
})
watch(multipleHeaders, (headers) => {
if (headers.length > 0 && (headers[headers.length - 1].key !== '' || headers[headers.length - 1].value !== '')) {
multipleHeaders.push({ key: '', value: '' })
}
}, {
deep: true,
immediate: true,
})
</script>
<template>
<Story
title="Field Key Values"
group="form"
:layout="{ type: 'grid', width: 600 }"
>
<template #controls>
<ThemeColorsHueControl />
</template>
<Variant
id="empty"
title="Empty"
>
<div>
<FieldKeyValues
v-model="emptyHeaders"
label="HTTP Headers"
description="Add custom HTTP headers"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(emptyHeaders, key, value)"
@remove="(index) => removeKeyValue(index, emptyHeaders)"
/>
</div>
</Variant>
<Variant
id="single-header"
title="Single Header"
>
<div>
<FieldKeyValues
v-model="singleHeader"
label="HTTP Headers"
description="Add custom HTTP headers"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(singleHeader, key, value)"
@remove="(index) => removeKeyValue(index, singleHeader)"
/>
</div>
</Variant>
<Variant
id="multiple-headers"
title="Multiple Headers"
>
<div>
<FieldKeyValues
v-model="multipleHeaders"
label="HTTP Headers"
description="Add custom HTTP headers"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(multipleHeaders, key, value)"
@remove="(index) => removeKeyValue(index, multipleHeaders)"
/>
</div>
</Variant>
<Variant
id="with-provider"
title="With Provider Name"
>
<div>
<FieldKeyValues
v-model="emptyHeaders"
label="API Headers"
description="Add custom headers for this API"
name="OpenAI"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(emptyHeaders, key, value)"
@remove="(index) => removeKeyValue(index, emptyHeaders)"
/>
</div>
</Variant>
<Variant
id="required"
title="Required Field"
>
<div>
<FieldKeyValues
v-model="emptyHeaders"
label="Required Headers"
description="These headers are required"
required
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(emptyHeaders, key, value)"
@remove="(index) => removeKeyValue(index, emptyHeaders)"
/>
</div>
</Variant>
<Variant
id="optional"
title="Optional Field"
>
<div>
<FieldKeyValues
v-model="emptyHeaders"
label="Optional Headers"
description="These headers are optional"
:required="false"
key-placeholder="Key"
value-placeholder="Value"
@add="(key, value) => addKeyValue(emptyHeaders, key, value)"
@remove="(index) => removeKeyValue(index, emptyHeaders)"
/>
</div>
</Variant>
</Story>
</template>
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import InputKeyValue from '../Input/InputKeyValue.vue'
const props = defineProps<{
label?: string
description?: string
name?: string
keyPlaceholder?: string
valuePlaceholder?: string
required?: boolean
inputClass?: string
}>()
const emit = defineEmits<{
(e: 'remove', index: number): void
(e: 'add', key: string, value: string): void
}>()
const keyValues = defineModel<{ key: string, value: string }[]>({ required: true })
const inputKey = ref('')
const inputValue = ref('')
watch([inputKey, inputValue], () => {
emit('add', inputKey.value, inputValue.value)
})
</script>
<template>
<div max-w-full>
<label flex="~ col gap-2">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ props.label }}
<span v-if="props.required !== false" class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
{{ props.description }}
</div>
</div>
<div flex="~ col gap-2">
<div
v-for="(keyValue, index) in keyValues"
:key="index"
v-auto-animate
w-full flex items-center gap-2
>
<InputKeyValue
v-model:property-key="keyValue.key"
v-model:property-value="keyValue.value"
:key-placeholder="props.keyPlaceholder"
:value-placeholder="props.valuePlaceholder"
w-full
/>
<button @click="emit('remove', index)">
<div i-solar:minus-circle-line-duotone size="6" />
</button>
</div>
</div>
</label>
</div>
</template>
@@ -27,7 +27,7 @@ function formatPercentage(value: number): string {
id="default-min"
title="Default (min)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldRange
v-model="minValue"
label="Minimum Value"
@@ -40,7 +40,7 @@ function formatPercentage(value: number): string {
id="default-mid"
title="Default (mid)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldRange
v-model="midValue"
label="Medium Value"
@@ -53,7 +53,7 @@ function formatPercentage(value: number): string {
id="default-max"
title="Default (max)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldRange
v-model="maxValue"
label="Maximum Value"
@@ -66,7 +66,7 @@ function formatPercentage(value: number): string {
id="custom-range"
title="Custom Range"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldRange
v-model="customValue"
label="Custom Range"
@@ -82,7 +82,7 @@ function formatPercentage(value: number): string {
id="formatted-value"
title="Formatted Value"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<FieldRange
v-model="midValue"
label="Formatted Value"
@@ -1,3 +1,4 @@
export { default as FieldCheckbox } from './FieldCheckbox.vue'
export { default as FieldInput } from './FieldInput.vue'
export { default as FieldKeyValues } from './FieldKeyValues.vue'
export { default as FieldRange } from './FieldRange.vue'
@@ -25,7 +25,7 @@ const urlValue = ref('https://example.com')
id="text-empty"
title="Text Input (Empty)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="textValue"
placeholder="Enter text..."
@@ -37,7 +37,7 @@ const urlValue = ref('https://example.com')
id="text-filled"
title="Text Input (Filled)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="filledTextValue"
placeholder="Enter text..."
@@ -49,7 +49,7 @@ const urlValue = ref('https://example.com')
id="password"
title="Password Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="passwordValue"
type="password"
@@ -62,7 +62,7 @@ const urlValue = ref('https://example.com')
id="email"
title="Email Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="emailValue"
type="email"
@@ -75,7 +75,7 @@ const urlValue = ref('https://example.com')
id="number"
title="Number Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="numberValue"
type="number"
@@ -88,7 +88,7 @@ const urlValue = ref('https://example.com')
id="url"
title="URL Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="urlValue"
type="url"
@@ -101,7 +101,7 @@ const urlValue = ref('https://example.com')
id="disabled"
title="Disabled Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="filledTextValue"
placeholder="This input is disabled"
@@ -114,7 +114,7 @@ const urlValue = ref('https://example.com')
id="readonly"
title="Readonly Input"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="filledTextValue"
placeholder="This input is readonly"
@@ -127,7 +127,7 @@ const urlValue = ref('https://example.com')
id="with-classes"
title="With Custom Classes"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="textValue"
placeholder="Custom styled input"
@@ -140,7 +140,7 @@ const urlValue = ref('https://example.com')
id="with-autofocus"
title="With Autofocus"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<Input
v-model="textValue"
placeholder="This input has autofocus"
@@ -0,0 +1,19 @@
<script setup lang="ts">
import Input from './Input.vue'
const props = defineProps<{
name?: string
keyPlaceholder?: string
valuePlaceholder?: string
}>()
const key = defineModel<string>('propertyKey', { required: true })
const value = defineModel<string>('propertyValue', { required: true })
</script>
<template>
<div flex="~ gap-2">
<Input v-model="key" :placeholder="props.keyPlaceholder" class="w-1/2" />
<Input v-model="value" :placeholder="props.valuePlaceholder" class="w-1/2" />
</div>
</template>
@@ -1 +1,2 @@
export { default as Input } from './Input.vue'
export { default as InputKeyValue } from './InputKeyValue.vue'
@@ -21,7 +21,7 @@ const filledAccountId = ref('1234567890abcdef1234567890abcdef')
id="empty"
title="Empty"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAccountIdInput
v-model="emptyAccountId"
placeholder="Your Cloudflare Account ID"
@@ -33,7 +33,7 @@ const filledAccountId = ref('1234567890abcdef1234567890abcdef')
id="filled"
title="Filled"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAccountIdInput
v-model="filledAccountId"
placeholder="Your Cloudflare Account ID"
@@ -45,7 +45,7 @@ const filledAccountId = ref('1234567890abcdef1234567890abcdef')
id="custom-label"
title="Custom Label"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAccountIdInput
v-model="emptyAccountId"
label="Cloudflare Account ID"
@@ -59,7 +59,7 @@ const filledAccountId = ref('1234567890abcdef1234567890abcdef')
id="not-required"
title="Not Required"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAccountIdInput
v-model="emptyAccountId"
:required="false"
@@ -16,7 +16,7 @@ import ProviderAdvancedSettings from './ProviderAdvancedSettings.vue'
id="collapsed"
title="Collapsed (Default)"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAdvancedSettings>
<div class="border border-neutral-200 rounded p-4 dark:border-neutral-800">
<p>Advanced settings content (initially hidden)</p>
@@ -29,7 +29,7 @@ import ProviderAdvancedSettings from './ProviderAdvancedSettings.vue'
id="expanded"
title="Expanded"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAdvancedSettings :initial-visible="true">
<div class="border border-neutral-200 rounded p-4 dark:border-neutral-800">
<p>Advanced settings content (initially visible)</p>
@@ -42,7 +42,7 @@ import ProviderAdvancedSettings from './ProviderAdvancedSettings.vue'
id="custom-title"
title="Custom Title"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderAdvancedSettings title="Expert Settings">
<div class="border border-neutral-200 rounded p-4 dark:border-neutral-800">
<p>Expert settings content</p>
@@ -21,7 +21,7 @@ const filledApiKey = ref('sk-1234567890abcdefghijklmnopqrstuvwxyz')
id="empty"
title="Empty"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderApiKeyInput
v-model="apiKey"
provider-name="OpenAI"
@@ -34,7 +34,7 @@ const filledApiKey = ref('sk-1234567890abcdefghijklmnopqrstuvwxyz')
id="filled"
title="Filled"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderApiKeyInput
v-model="filledApiKey"
provider-name="OpenAI"
@@ -47,7 +47,7 @@ const filledApiKey = ref('sk-1234567890abcdefghijklmnopqrstuvwxyz')
id="custom-label"
title="Custom Label"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderApiKeyInput
v-model="apiKey"
provider-name="OpenAI"
@@ -62,7 +62,7 @@ const filledApiKey = ref('sk-1234567890abcdefghijklmnopqrstuvwxyz')
id="not-required"
title="Not Required"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderApiKeyInput
v-model="apiKey"
provider-name="OpenAI"
@@ -22,7 +22,7 @@ const customUrl = ref('https://custom-api.example.com/v1/')
id="empty"
title="Empty"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBaseUrlInput
v-model="emptyUrl"
placeholder="https://api.example.com/v1/"
@@ -34,7 +34,7 @@ const customUrl = ref('https://custom-api.example.com/v1/')
id="default-url"
title="Default URL"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBaseUrlInput
v-model="defaultUrl"
placeholder="https://api.openai.com/v1/"
@@ -46,7 +46,7 @@ const customUrl = ref('https://custom-api.example.com/v1/')
id="custom-url"
title="Custom URL"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBaseUrlInput
v-model="customUrl"
placeholder="https://api.example.com/v1/"
@@ -58,7 +58,7 @@ const customUrl = ref('https://custom-api.example.com/v1/')
id="required"
title="Required"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBaseUrlInput
v-model="emptyUrl"
placeholder="https://api.example.com/v1/"
@@ -71,7 +71,7 @@ const customUrl = ref('https://custom-api.example.com/v1/')
id="custom-label"
title="Custom Label"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBaseUrlInput
v-model="emptyUrl"
label="API Endpoint"
@@ -16,7 +16,7 @@ import ProviderBasicSettings from './ProviderBasicSettings.vue'
id="default"
title="Default"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBasicSettings>
<div class="border border-neutral-200 rounded p-4 dark:border-neutral-800">
<p>Settings content goes here</p>
@@ -29,7 +29,7 @@ import ProviderBasicSettings from './ProviderBasicSettings.vue'
id="with-title-description"
title="With Title & Description"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBasicSettings
title="Custom Title"
description="Custom description for this section"
@@ -45,7 +45,7 @@ import ProviderBasicSettings from './ProviderBasicSettings.vue'
id="with-reset"
title="With Reset Button"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderBasicSettings
title="Basic Settings"
description="Essential settings for this provider"
@@ -18,7 +18,7 @@ import ProviderSettingsContainer from './ProviderSettingsContainer.vue'
id="empty"
title="Empty"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderSettingsContainer>
<div class="p-4">
<p>Container content goes here</p>
@@ -31,7 +31,7 @@ import ProviderSettingsContainer from './ProviderSettingsContainer.vue'
id="with-sections"
title="With Sections"
>
<div class="bg-white p-4 dark:bg-neutral-900">
<div>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
@@ -16,7 +16,7 @@ import ProviderSettingsLayout from './ProviderSettingsLayout.vue'
id="default"
title="Default"
>
<div class="p-4">
<div>
<ProviderSettingsLayout provider-name="Example Provider">
<div class="mt-4 rounded-xl bg-neutral-50 p-4 dark:bg-[rgba(0,0,0,0.3)]">
<p>Provider settings content goes here</p>
@@ -29,7 +29,7 @@ import ProviderSettingsLayout from './ProviderSettingsLayout.vue'
id="with-icon"
title="With Icon"
>
<div class="p-4">
<div>
<ProviderSettingsLayout
provider-name="OpenAI" provider-icon="i-lobe-icons:openai"
>
@@ -44,7 +44,7 @@ import ProviderSettingsLayout from './ProviderSettingsLayout.vue'
id="with-icon-color"
title="With Icon Color"
>
<div class="p-4">
<div>
<ProviderSettingsLayout
provider-name="Mistral AI" provider-icon-color="i-lobe-icons:mistral-color"
>
+4 -4
View File
@@ -161,8 +161,8 @@ export const useProvidersStore = defineStore('providers', () => {
},
},
},
'ollama-ai': {
id: 'ollama-ai',
'ollama': {
id: 'ollama',
nameKey: 'providers.ollama.name',
name: 'Ollama',
descriptionKey: 'providers.ollama.description',
@@ -180,7 +180,7 @@ export const useProvidersStore = defineStore('providers', () => {
return {
id: model.id,
name: model.id,
provider: 'ollama-ai',
provider: 'ollama',
description: '',
contextLength: 0,
deprecated: false,
@@ -538,7 +538,7 @@ export const useProvidersStore = defineStore('providers', () => {
return !!config.apiKey && !!config.baseUrl
case 'openai':
return !!config.apiKey
case 'ollama-ai':
case 'ollama':
return !!config.baseUrl
case 'vllm':
return !!config.baseUrl
+5
View File
@@ -1,4 +1,6 @@
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { defineSetupVue3 } from '@histoire/plugin-vue'
import { MotionPlugin } from '@vueuse/motion'
import ThemeColorsHueControl from './ThemeColorsHueControl.vue'
@@ -7,5 +9,8 @@ import '@unocss/reset/tailwind.css'
import './main.css'
export const setupVue3 = defineSetupVue3(({ app }) => {
app.use(MotionPlugin)
app.use(autoAnimatePlugin)
app.component('ThemeColorsHueControl', ThemeColorsHueControl)
})
+6
View File
@@ -1053,9 +1053,15 @@ importers:
packages/stage-ui:
dependencies:
'@vueuse/motion':
specifier: ^3.0.3
version: 3.0.3(magicast@0.3.5)(rollup@4.35.0)(vue@3.5.13(typescript@5.8.2))
reka-ui:
specifier: ^2.1.0
version: 2.1.0(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
vue:
specifier: link:@formkit/auto-animate/vue
version: 3.5.13(typescript@5.8.2)
devDependencies:
'@electron-toolkit/preload':
specifier: ^3.0.1