feat(stage-ui|stage-tamagotchi): support xAI, DeepSeek, Fireworks AI, Novita, Together AI, Moonshot AI, Mistral

This commit is contained in:
Neko Ayaka
2025-03-16 12:48:55 +08:00
parent 6f8a7ff502
commit 97b33c17bd
34 changed files with 2393 additions and 1046 deletions
@@ -0,0 +1,97 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAccountIdInput,
ProviderApiKeyInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, 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 = 'cloudflare-workers-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const accountId = computed({
get: () => providers.value[providerId]?.accountId || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].accountId = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
accountId.value = providers.value[providerId]?.accountId || ''
})
// Watch settings and update the provider configuration
watch([apiKey, accountId], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
accountId: accountId.value,
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="Your Cloudflare API token"
/>
<ProviderAccountIdInput
v-model="accountId"
label="Account ID"
description="Cloudflare Account ID"
placeholder="Your Cloudflare Account ID"
/>
</ProviderBasicSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'deepseek'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.deepseek.com/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -4,10 +4,18 @@ import type { RemovableRef } from '@vueuse/core'
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import { Collapsable, Range } from '@proj-airi/stage-ui/components'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderCheckboxInput,
ProviderSettingsContainer,
ProviderSettingsLayout,
ProviderSliderInput,
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { generateSpeech } from '@xsai/generate-speech'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -115,9 +123,6 @@ const selectedLanguage = ref(speechStore.selectedLanguage)
const selectedVoice = ref(speechStore.voiceName)
const availableVoices = computed(() => speechStore.availableVoicesForLanguage)
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
providersStore.initializeProvider(providerId)
@@ -239,300 +244,173 @@ function handleResetVoiceSettings() {
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName }}
</div>
</h1>
</div>
<div flex="~ col md:row gap-6">
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6" w="full md:40%">
<div>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<button
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="handleResetVoiceSettings"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
</div>
</div>
<input
v-model="apiKey"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="sk-"
>
</label>
</div>
</div>
</div>
<div flex="~ col gap-6">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Settings
</h2>
<div flex="~ col gap-4">
<label flex="~ col gap-2">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Similarity Boost
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Voice similarity adherence
</div>
</div>
<span font-mono>{{ similarityBoost.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="similarityBoost" :min="0" :max="1" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Stability
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Voice stability and randomness
</div>
</div>
<span font-mono>{{ stability.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="stability" :min="0" :max="1" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Speed
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Speech generation speed
</div>
</div>
<span font-mono>{{ speed.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="speed" :min="0.7" :max="1.2" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Style
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Voice style exaggeration
</div>
</div>
<span font-mono>{{ style.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="style" :min="0" :max="1" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Speaker Boost
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Enhance speaker similarity
</div>
</div>
<span font-mono>{{ useSpeakerBoost ? 'On' : 'Off' }}</span>
</div>
<div flex="~ row" items-center gap-2>
<input v-model="useSpeakerBoost" type="checkbox">
</div>
</label>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div flex="~ col gap-6" w="full md:60%">
<div rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Playground
</h2>
<div flex="~ col gap-4">
<textarea
v-model="testText" placeholder="Enter text to test the voice..."
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
h-24 w-full rounded-lg px-3 py-2 text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
<div flex="~ col md:row gap-6">
<ProviderSettingsContainer class="w-full md:w-[40%]">
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetVoiceSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-"
/>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Language
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select voice language
</div>
</div>
<select
v-model="selectedLanguage"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="language in speechStore.availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
</ProviderBasicSettings>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Voice
<div flex="~ col gap-6">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Settings
</h2>
<div flex="~ col gap-4">
<ProviderSliderInput
v-model="similarityBoost"
label="Similarity Boost"
description="Voice similarity adherence"
:min="0"
:max="1"
:step="0.01"
/>
<ProviderSliderInput
v-model="stability"
label="Stability"
description="Voice stability and randomness"
:min="0"
:max="1"
:step="0.01"
/>
<ProviderSliderInput
v-model="speed"
label="Speed"
description="Speech generation speed"
:min="0.7"
:max="1.2"
:step="0.01"
/>
<ProviderSliderInput
v-model="style"
label="Style"
description="Voice style exaggeration"
:min="0"
:max="1"
:step="0.01"
/>
<ProviderCheckboxInput
v-model="useSpeakerBoost"
label="Speaker Boost"
description="Enhance speaker similarity"
/>
</div>
</div>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
required
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
<div flex="~ col gap-6" class="w-full md:w-[60%]">
<div rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Playground
</h2>
<div flex="~ col gap-4">
<textarea
v-model="testText" placeholder="Enter text to test the voice..."
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
h-24 w-full rounded-lg px-3 py-2 text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
/>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Language
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select voice language
</div>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select preferred voice
<select
v-model="selectedLanguage"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="language in speechStore.availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Voice
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select preferred voice
</div>
</div>
</div>
<select
v-model="selectedVoice"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
<select
v-model="selectedVoice"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
</label>
</div>
<div flex="~ row" gap-4>
<button
border="neutral-800 dark:neutral-200 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4
text="neutral-100 dark:neutral-900"
py-2 text-sm :disabled="isGenerating || !testText.trim() || !apiKey"
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || !testText.trim() || !apiKey }"
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
</label>
<div flex="~ row" items-center gap-2>
<div i-solar:play-circle-bold-duotone />
<span>{{ isGenerating ? 'Generating...' : 'Test Voice' }}</span>
</div>
</button>
<button
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
>
<div flex="~ row" items-center gap-2>
<div i-solar:stop-circle-bold-duotone />
<span>Stop</span>
</div>
</button>
</div>
<div v-if="!apiKey" class="mt-2 text-sm text-red-500">
Please enter an API key to test the voice.
</div>
<div v-if="errorMessage" class="mt-2 text-sm text-red-500">
{{ errorMessage }}
</div>
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
</div>
<div flex="~ row" gap-4>
<button
border="neutral-800 dark:neutral-200 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4
text="neutral-100 dark:neutral-900"
py-2 text-sm :disabled="isGenerating || !testText.trim() || !apiKey"
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || !testText.trim() || !apiKey }"
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
>
<div flex="~ row" items-center gap-2>
<div i-solar:play-circle-bold-duotone />
<span>{{ isGenerating ? 'Generating...' : 'Test Voice' }}</span>
</div>
</button>
<button
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
>
<div flex="~ row" items-center gap-2>
<div i-solar:stop-circle-bold-duotone />
<span>Stop</span>
</div>
</button>
</div>
<div v-if="!apiKey" class="mt-2 text-sm text-red-500">
Please enter an API key to test the voice.
</div>
<div v-if="errorMessage" class="mt-2 text-sm text-red-500">
{{ errorMessage }}
</div>
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
</div>
</div>
</div>
</div>
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" :class="providerMetadata?.icon" />
</div>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'fireworks-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</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"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="fw-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.fireworks.ai/inference/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'mistral-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="mis-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.mistral.ai/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'moonshot-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</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"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="ms-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.moonshot.cn/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'novita-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="nvt-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.novita.ai/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -1,11 +1,17 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import { Collapsable } from '@proj-airi/stage-ui/components'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -37,9 +43,6 @@ const baseUrl = computed({
},
})
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
// Initialize provider if it doesn't exist
if (!providers.value[providerId]) {
@@ -70,115 +73,30 @@ function handleResetSettings() {
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName || 'OpenAI' }}
</div>
</h1>
</div>
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6">
<div>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<button
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="handleResetSettings"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName || 'OpenAI' }}
</div>
</div>
<input
v-model="apiKey"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="sk-..."
>
</label>
</div>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl"
type="text"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="https://api.openai.com/v1/"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" i-lobe-icons:openai />
</div>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.openai.com/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -1,11 +1,17 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import { Collapsable } from '@proj-airi/stage-ui/components'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -37,9 +43,6 @@ const baseUrl = computed({
},
})
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
providersStore.initializeProvider(providerId)
@@ -65,115 +68,30 @@ function handleResetSettings() {
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName }}
</div>
</h1>
</div>
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6">
<div>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<button
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="handleResetSettings"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
</div>
</div>
<input
v-model="apiKey"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="sk-or-..."
>
</label>
</div>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl"
type="text"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" :class="providerMetadata?.icon" />
</div>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-or-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'together-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="togetherapi-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.together.xyz/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'xai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</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"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="xai-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.x.ai/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,97 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAccountIdInput,
ProviderApiKeyInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, 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 = 'cloudflare-workers-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const accountId = computed({
get: () => providers.value[providerId]?.accountId || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].accountId = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
accountId.value = providers.value[providerId]?.accountId || ''
})
// Watch settings and update the provider configuration
watch([apiKey, accountId], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
accountId: accountId.value,
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="Your Cloudflare API token"
/>
<ProviderAccountIdInput
v-model="accountId"
label="Account ID"
description="Cloudflare Account ID"
placeholder="Your Cloudflare Account ID"
/>
</ProviderBasicSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'deepseek'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.deepseek.com/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -4,10 +4,18 @@ import type { RemovableRef } from '@vueuse/core'
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import { Collapsable, Range } from '@proj-airi/stage-ui/components'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderCheckboxInput,
ProviderSettingsContainer,
ProviderSettingsLayout,
ProviderSliderInput,
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { generateSpeech } from '@xsai/generate-speech'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -115,9 +123,6 @@ const selectedLanguage = ref(speechStore.selectedLanguage)
const selectedVoice = ref(speechStore.voiceName)
const availableVoices = computed(() => speechStore.availableVoicesForLanguage)
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
providersStore.initializeProvider(providerId)
@@ -239,300 +244,173 @@ function handleResetVoiceSettings() {
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName }}
</div>
</h1>
</div>
<div flex="~ col md:row gap-6">
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6" w="full md:40%">
<div>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<button
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="handleResetVoiceSettings"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
</div>
</div>
<input
v-model="apiKey"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="sk-"
>
</label>
</div>
</div>
</div>
<div flex="~ col gap-6">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Settings
</h2>
<div flex="~ col gap-4">
<label flex="~ col gap-2">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Similarity Boost
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Voice similarity adherence
</div>
</div>
<span font-mono>{{ similarityBoost.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="similarityBoost" :min="0" :max="1" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Stability
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Voice stability and randomness
</div>
</div>
<span font-mono>{{ stability.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="stability" :min="0" :max="1" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Speed
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Speech generation speed
</div>
</div>
<span font-mono>{{ speed.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="speed" :min="0.7" :max="1.2" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Style
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Voice style exaggeration
</div>
</div>
<span font-mono>{{ style.toFixed(2) }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range v-model="style" :min="0" :max="1" :step="0.01" w-full />
</div>
</label>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
Speaker Boost
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Enhance speaker similarity
</div>
</div>
<span font-mono>{{ useSpeakerBoost ? 'On' : 'Off' }}</span>
</div>
<div flex="~ row" items-center gap-2>
<input v-model="useSpeakerBoost" type="checkbox">
</div>
</label>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div flex="~ col gap-6" w="full md:60%">
<div rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Playground
</h2>
<div flex="~ col gap-4">
<textarea
v-model="testText" placeholder="Enter text to test the voice..."
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
h-24 w-full rounded-lg px-3 py-2 text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
<div flex="~ col md:row gap-6">
<ProviderSettingsContainer class="w-full md:w-[40%]">
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetVoiceSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-"
/>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Language
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select voice language
</div>
</div>
<select
v-model="selectedLanguage"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="language in speechStore.availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
</ProviderBasicSettings>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Voice
<div flex="~ col gap-6">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Settings
</h2>
<div flex="~ col gap-4">
<ProviderSliderInput
v-model="similarityBoost"
label="Similarity Boost"
description="Voice similarity adherence"
:min="0"
:max="1"
:step="0.01"
/>
<ProviderSliderInput
v-model="stability"
label="Stability"
description="Voice stability and randomness"
:min="0"
:max="1"
:step="0.01"
/>
<ProviderSliderInput
v-model="speed"
label="Speed"
description="Speech generation speed"
:min="0.7"
:max="1.2"
:step="0.01"
/>
<ProviderSliderInput
v-model="style"
label="Style"
description="Voice style exaggeration"
:min="0"
:max="1"
:step="0.01"
/>
<ProviderCheckboxInput
v-model="useSpeakerBoost"
label="Speaker Boost"
description="Enhance speaker similarity"
/>
</div>
</div>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
required
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
<div flex="~ col gap-6" class="w-full md:w-[60%]">
<div rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Playground
</h2>
<div flex="~ col gap-4">
<textarea
v-model="testText" placeholder="Enter text to test the voice..."
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
h-24 w-full rounded-lg px-3 py-2 text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
/>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Language
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select voice language
</div>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select preferred voice
<select
v-model="selectedLanguage"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="language in speechStore.availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Voice
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
Select preferred voice
</div>
</div>
</div>
<select
v-model="selectedVoice"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
<select
v-model="selectedVoice"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
</label>
</div>
<div flex="~ row" gap-4>
<button
border="neutral-800 dark:neutral-200 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4
text="neutral-100 dark:neutral-900"
py-2 text-sm :disabled="isGenerating || !testText.trim() || !apiKey"
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || !testText.trim() || !apiKey }"
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
</label>
<div flex="~ row" items-center gap-2>
<div i-solar:play-circle-bold-duotone />
<span>{{ isGenerating ? 'Generating...' : 'Test Voice' }}</span>
</div>
</button>
<button
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
>
<div flex="~ row" items-center gap-2>
<div i-solar:stop-circle-bold-duotone />
<span>Stop</span>
</div>
</button>
</div>
<div v-if="!apiKey" class="mt-2 text-sm text-red-500">
Please enter an API key to test the voice.
</div>
<div v-if="errorMessage" class="mt-2 text-sm text-red-500">
{{ errorMessage }}
</div>
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
</div>
<div flex="~ row" gap-4>
<button
border="neutral-800 dark:neutral-200 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4
text="neutral-100 dark:neutral-900"
py-2 text-sm :disabled="isGenerating || !testText.trim() || !apiKey"
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || !testText.trim() || !apiKey }"
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
>
<div flex="~ row" items-center gap-2>
<div i-solar:play-circle-bold-duotone />
<span>{{ isGenerating ? 'Generating...' : 'Test Voice' }}</span>
</div>
</button>
<button
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
>
<div flex="~ row" items-center gap-2>
<div i-solar:stop-circle-bold-duotone />
<span>Stop</span>
</div>
</button>
</div>
<div v-if="!apiKey" class="mt-2 text-sm text-red-500">
Please enter an API key to test the voice.
</div>
<div v-if="errorMessage" class="mt-2 text-sm text-red-500">
{{ errorMessage }}
</div>
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
</div>
</div>
</div>
</div>
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" :class="providerMetadata?.icon" />
</div>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'fireworks-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</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"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="fw-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.fireworks.ai/inference/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'mistral-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="mis-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.mistral.ai/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'moonshot-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</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"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="ms-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.moonshot.cn/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'novita-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="nvt-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.novita.ai/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -1,11 +1,17 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import { Collapsable } from '@proj-airi/stage-ui/components'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -37,9 +43,6 @@ const baseUrl = computed({
},
})
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
// Initialize provider if it doesn't exist
if (!providers.value[providerId]) {
@@ -70,115 +73,30 @@ function handleResetSettings() {
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName || 'OpenAI' }}
</div>
</h1>
</div>
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6">
<div>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<button
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="handleResetSettings"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName || 'OpenAI' }}
</div>
</div>
<input
v-model="apiKey"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="sk-..."
>
</label>
</div>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl"
type="text"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="https://api.openai.com/v1/"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" i-lobe-icons:openai />
</div>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName || 'OpenAI'"
placeholder="sk-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.openai.com/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -1,11 +1,17 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import { Collapsable } from '@proj-airi/stage-ui/components'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -37,9 +43,6 @@ const baseUrl = computed({
},
})
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
providersStore.initializeProvider(providerId)
@@ -65,115 +68,30 @@ function handleResetSettings() {
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerMetadata?.localizedName }}
</div>
</h1>
</div>
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6">
<div>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Basic
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Essential settings</span>
</div>
</div>
<button
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="handleResetSettings"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
API Key for {{ providerMetadata?.localizedName }}
</div>
</div>
<input
v-model="apiKey"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
placeholder="sk-or-..."
>
</label>
</div>
</div>
</div>
<div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl"
type="text"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
>
</label>
</div>
</Collapsable>
</div>
</div>
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" :class="providerMetadata?.icon" />
</div>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="sk-or-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'together-ai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon-color="providerMetadata?.iconColor"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
title="Basic"
description="Essential settings"
:on-reset="handleResetSettings"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="togetherapi-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.together.xyz/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
import {
ProviderAdvancedSettings,
ProviderApiKeyInput,
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, 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 = 'xai'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
// Use computed properties for settings
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
onMounted(() => {
// Initialize provider if it doesn't exist
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || ''
})
// Watch settings and update the provider configuration
watch([apiKey, baseUrl], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || '',
}
})
function handleResetSettings() {
providers.value[providerId] = {
...(providerMetadata.value?.defaultOptions as any),
}
}
</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"
>
<ProviderApiKeyInput
v-model="apiKey"
:provider-name="providerMetadata?.localizedName"
placeholder="xai-..."
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings title="Advanced">
<ProviderBaseUrlInput
v-model="baseUrl"
placeholder="https://api.x.ai/v1/"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</template>
+1
View File
@@ -2,6 +2,7 @@ import { HstVue } from '@histoire/plugin-vue'
import { defineConfig } from 'histoire'
export default defineConfig({
routerMode: 'hash',
plugins: [
HstVue(),
],
@@ -4,6 +4,7 @@ export * from './Form'
export { default as Live2DCanvas } from './Live2D/Canvas.vue'
export { default as Live2DModel } from './Live2D/Model.vue'
export * from './Menu'
export * from './providers'
export { default as SceneLive2D } from './Scenes/Live2D.vue'
export { default as SceneVRM } from './Scenes/VRM.vue'
export { default as TransitionVertical } from './TransitionVertical.vue'
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
modelValue: string
placeholder?: string
required?: boolean
label?: string
description?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const value = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
})
</script>
<template>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ label || 'Account ID' }}
<span v-if="required !== false" class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
{{ description || 'Account ID' }}
</div>
</div>
<input
v-model="value"
type="text"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="placeholder"
>
</label>
</div>
</template>
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { Collapsable } from '@proj-airi/stage-ui/components'
import { ref } from 'vue'
const props = defineProps<{
title?: string
initialVisible?: boolean
}>()
const visible = ref(props.initialVisible || false)
function toggleVisible() {
visible.value = !visible.value
}
</script>
<template>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleVisible()"
>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
<span>{{ title || 'Advanced' }}</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<slot />
</div>
</Collapsable>
</template>
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
modelValue: string
providerName: string
placeholder?: string
required?: boolean
label?: string
description?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const value = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
})
</script>
<template>
<div max-w-full>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ label || 'API Key' }}
<span v-if="required !== false" class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
{{ description || `API Key for ${providerName}` }}
</div>
</div>
<input
v-model="value"
type="password"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="placeholder"
>
</label>
</div>
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
modelValue: string
placeholder?: string
required?: boolean
label?: string
description?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const value = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
})
</script>
<template>
<label flex="~ col gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ label || 'Base URL' }}
<span v-if="required" class="text-red-500">*</span>
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400" text-nowrap>
{{ description || 'Custom base URL (optional)' }}
</div>
</div>
<input
v-model="value"
type="text"
border="neutral-200 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="all duration-250 ease-in-out"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
bg="neutral-100 dark:neutral-800 focus:white dark:focus:neutral-700"
:placeholder="placeholder"
>
</label>
</template>
@@ -0,0 +1,34 @@
<script setup lang="ts">
defineProps<{
title?: string
description?: string
onReset?: () => void
}>()
</script>
<template>
<div flex="~ col gap-6">
<div flex="~ row" items-center justify-between>
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
{{ title || 'Basic' }}
</h2>
<div text="neutral-400 dark:neutral-500">
<span>{{ description || 'Essential settings' }}</span>
</div>
</div>
<button
v-if="onReset"
title="Reset settings"
flex items-center justify-center rounded-full p-2
transition="all duration-250 ease-in-out"
text="neutral-500 dark:neutral-400"
bg="transparent dark:transparent hover:neutral-200 dark:hover:neutral-800 active:neutral-300 dark:active:neutral-700"
@click="onReset"
>
<div i-solar:refresh-bold-duotone text-xl />
</button>
</div>
<slot />
</div>
</template>
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
modelValue: boolean
label?: string
description?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const value = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
})
</script>
<template>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
{{ label }}
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
{{ description }}
</div>
</div>
<span font-mono>{{ value ? 'On' : 'Off' }}</span>
</div>
<div flex="~ row" items-center gap-2>
<input v-model="value" type="checkbox">
</div>
</label>
</template>
@@ -0,0 +1,9 @@
<script setup lang="ts">
// No props needed for this container component
</script>
<template>
<div bg="neutral-50 dark:[rgba(0,0,0,0.3)]" rounded-xl p-4 flex="~ col gap-6">
<slot />
</div>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
defineProps<{
providerName: string
providerIcon?: string
providerIconColor?: string
onBack?: () => void
}>()
</script>
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
>
<button @click="onBack">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
</div>
<div text-3xl font-semibold>
{{ providerName }}
</div>
</h1>
</div>
<slot />
<div text="neutral-100/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 translate-x-10 translate-y-10>
<div text="40" :class="providerIcon || providerIconColor" />
</div>
</template>
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { Range } from '@proj-airi/stage-ui/components'
import { computed } from 'vue'
const props = defineProps<{
modelValue: number
min?: number
max?: number
step?: number
label?: string
description?: string
formatValue?: (value: number) => string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>()
const value = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
})
const formattedValue = computed(() => {
if (props.formatValue) {
return props.formatValue(value.value)
}
return value.value.toFixed(2)
})
</script>
<template>
<label flex="~ col gap-4">
<div flex="~ row" items-center gap-2>
<div flex="1">
<div class="flex items-center gap-1 text-sm font-medium">
{{ label }}
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
{{ description }}
</div>
</div>
<span font-mono>{{ formattedValue }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range
v-model="value"
:min="min || 0"
:max="max || 1"
:step="step || 0.01"
w-full
/>
</div>
</label>
</template>
@@ -0,0 +1,9 @@
export { default as ProviderAccountIdInput } from './ProviderAccountIdInput.vue'
export { default as ProviderAdvancedSettings } from './ProviderAdvancedSettings.vue'
export { default as ProviderApiKeyInput } from './ProviderApiKeyInput.vue'
export { default as ProviderBaseUrlInput } from './ProviderBaseUrlInput.vue'
export { default as ProviderBasicSettings } from './ProviderBasicSettings.vue'
export { default as ProviderCheckboxInput } from './ProviderCheckboxInput.vue'
export { default as ProviderSettingsContainer } from './ProviderSettingsContainer.vue'
export { default as ProviderSettingsLayout } from './ProviderSettingsLayout.vue'
export { default as ProviderSliderInput } from './ProviderSliderInput.vue'