feat(stage-ui|stage-tamagotchi|stage-web): ollama, header & values
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Generated
+6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user