feat(stage-web): better settings

Signed-off-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
Neko Ayaka
2025-03-07 00:39:17 +08:00
parent 76b166e65b
commit 0ccd164dde
12 changed files with 555 additions and 14 deletions
+1
View File
@@ -93,6 +93,7 @@
"@iconify-json/simple-icons": "^1.2.26",
"@iconify-json/solar": "^1.2.2",
"@iconify-json/svg-spinners": "^1.2.2",
"@iconify-json/vscode-icons": "^1.2.16",
"@iconify/utils": "^2.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@proj-airi/drizzle-duckdb-wasm": "workspace:^",
Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

+18 -10
View File
@@ -2,7 +2,8 @@
defineProps<{
title: string
description: string
icon: string
icon?: string
iconTemplate?: boolean
to: string
}>()
</script>
@@ -15,19 +16,26 @@ defineProps<{
:to="to"
>
<div z-1 flex-1>
<div text-lg font-semibold>
<div text-lg font-bold>
{{ title }}
</div>
<div text="sm neutral-600 dark:neutral-400">
<div text="sm neutral-500 dark:neutral-400">
<span>{{ description }}</span>
</div>
</div>
<div
class="settings-section-icon"
transition="all ease-in-out duration-500"
absolute right-0 size-24 translate-y-4
text="neutral-400/50 dark:neutral-600/50"
:class="[icon]"
/>
<template v-if="typeof icon === 'string'">
<div
class="settings-section-icon"
transition="all ease-in-out duration-500"
absolute right-0 size-24 translate-y-4
text="neutral-400/50 dark:neutral-600/50"
:class="[icon]"
/>
</template>
<template v-if="iconTemplate">
<slot
name="icon"
/>
</template>
</RouterLink>
</template>
@@ -25,6 +25,7 @@ function handleLanguageChange(event: Event) {
<div flex="~ col gap-4">
<div flex="~ col gap-4">
<IconItem title="Modules" description="Thinking, vision, speech synthesis, gaming, etc." icon="i-lucide:blocks" to="/settings/modules" />
<IconItem title="Models" description="Live2D, VRM, etc." icon="i-lucide:person-standing" to="/settings/models" />
<IconItem title="Providers" description="LLMs, speech providers, etc." icon="i-lucide:brain" to="/settings/providers" />
<IconItem title="Themes" description="Customize your stage!" icon="i-lucide:paintbrush" to="/settings/themes" />
</div>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-800">Settings</span>
</div>
<div text-3xl font-semibold>
Models
</div>
</h1>
</div>
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lucide:person-standing translate-x-10 translate-y-10 />
</div>
</template>
<route lang="yaml">
meta:
stageTransition:
name: slide
</route>
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-800">Modules</span>
</div>
<div text-3xl font-semibold>
Consciousness
</div>
</h1>
</div>
<div>
<div>
<span>Provider</span>
<div>
<div>
<span>OpenAI</span>
</div>
</div>
</div>
</div>
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lucide:ghost translate-x-10 translate-y-10 />
</div>
</template>
<route lang="yaml">
meta:
stageTransition:
name: slide
</route>
@@ -1,6 +1,140 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import FactorioIcon from '../../../assets/icons/modules/games/factorio.png'
const router = useRouter()
interface Module {
id: string
name: string
description: string
icon: string | { imagePath: string }
to: string
configured: boolean
}
const modulesList = computed<Module[]>(() => [
{
id: 'consciousness',
name: 'Consciousness',
description: 'Thinking, vision, speech synthesis, gaming, etc.',
icon: 'i-lucide:ghost',
to: '/settings/modules/consciousness',
configured: false,
},
{
id: 'hearing',
name: 'Hearing',
description: 'Hearing, speech recognition, etc.',
icon: 'i-lucide:ear',
to: '',
configured: false,
},
{
id: 'messaging-discord',
name: 'Discord',
description: 'Messaging, notifications, etc.',
icon: 'i-simple-icons:discord',
to: '',
configured: false,
},
{
id: 'speech',
name: 'Speech',
description: 'Speech synthesis, etc.',
icon: 'i-lucide:mic',
to: '',
configured: false,
},
{
id: 'memory-short-term',
name: 'Short-Term Memory',
description: 'Short-term memory, etc.',
icon: 'i-lucide:book',
to: '',
configured: false,
},
{
id: 'memory-long-term',
name: 'Long-Term Memory',
description: 'Long-term memory, etc.',
icon: 'i-lucide:book-copy',
to: '',
configured: false,
},
{
id: 'vision',
name: 'Vision',
description: 'Vision, etc.',
icon: 'i-lucide:eye',
to: '',
configured: false,
},
{
id: 'game-minecraft',
name: 'Minecraft',
description: 'Playing Minecraft with you, etc.',
icon: 'i-vscode-icons:file-type-minecraft',
to: '',
configured: false,
},
{
id: 'game-factorio',
name: 'Factorio',
description: 'Playing Factorio with you, etc.',
icon: {
imagePath: FactorioIcon,
},
to: '',
configured: false,
},
])
</script>
<template>
<div>
A
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-800">Settings</span>
</div>
<div text-3xl font-semibold>
Modules
</div>
</h1>
</div>
<div grid="~ cols-1 gap-2">
<div
v-for="module in modulesList" :key="module.id"
bg="neutral-300/50 dark:neutral-600" w-full of-hidden rounded-xl
flex="~ col 1"
>
<template v-if="typeof module.icon === 'string'">
<IconItem :title="module.name" :description="module.description" :icon="module.icon" :to="module.to" />
</template>
<template v-else>
<IconItem :title="module.name" :description="module.description" icon-template :to="module.to">
<template #icon>
<img
:src="module.icon.imagePath"
transition="all ease-in-out duration-500"
absolute right-0 size-24 translate-y-4
>
</template>
</IconItem>
</template>
<div p-2>
<div v-if="module.configured" size-3 bg="green-500 dark:green-600" rounded-full />
<div v-else size-3 bg="neutral-400 dark:neutral-500" rounded-full />
</div>
</div>
</div>
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lucide:blocks translate-x-10 translate-y-10 />
</div>
</template>
@@ -1,6 +1,162 @@
<script setup lang="ts">
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
interface ModelProvider {
id: string
name: string
icon?: string
to: string
configured: boolean
}
const router = useRouter()
const providersStore = useProvidersStore()
const { configuredForOpenRouter } = storeToRefs(providersStore)
const providersList = computed<ModelProvider[]>(() => [
{
id: 'openrouter-ai',
name: 'OpenRouter',
icon: 'i-lobe-icons:openrouter',
to: '/settings/providers/openrouter',
configured: configuredForOpenRouter.value,
},
{
id: 'openai',
name: 'OpenAI',
icon: 'i-lobe-icons:openai',
to: '/settings/providers/openai',
configured: false,
},
{
id: 'ollama-ai',
name: 'Ollama',
icon: 'i-lobe-icons:ollama',
to: '/settings/providers/ollama',
configured: false,
},
{
id: 'vllm',
name: 'vLLM',
icon: 'i-lobe-icons:vllm-color',
to: '/settings/providers/vllm',
configured: false,
},
{
id: 'elevenlabs',
name: 'ElevenLabs',
icon: 'i-simple-icons:elevenlabs',
to: '/settings/providers/elevenlabs',
configured: false,
},
{
id: 'xai',
name: 'xAI',
icon: 'i-lobe-icons:xai',
to: '/settings/providers/xai',
configured: false,
},
{
id: 'deepseek',
name: 'DeepSeek',
icon: 'i-lobe-icons:deepseek-color',
to: '/settings/providers/deepseek',
configured: false,
},
{
id: 'together-ai',
name: 'Together.ai',
icon: 'i-lobe-icons:together-color',
to: '/settings/providers/together',
configured: false,
},
{
id: 'novita-ai',
name: 'Novita',
icon: 'i-lobe-icons:novita-color',
to: '/settings/providers/novita',
configured: false,
},
{
id: 'fireworks-ai',
name: 'Fireworks.ai',
icon: 'i-lobe-icons:fireworks',
to: '/settings/providers/fireworks',
configured: false,
},
{
id: 'cloudflare-workers-ai',
name: 'Cloudflare Workers AI',
icon: 'i-lobe-icons:cloudflare-color',
to: '/settings/providers/cloudflare',
configured: false,
},
{
id: 'mistral-ai',
name: 'Mistral',
icon: 'i-lobe-icons:mistral-color',
to: '/settings/providers/mistral',
configured: false,
},
{
id: 'moonshot-ai',
name: 'Moonshot AI',
icon: 'i-lobe-icons:moonshot',
to: '/settings/providers/moonshot',
configured: false,
},
])
</script>
<template>
<div>
A
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-800">Settings</span>
</div>
<div text-3xl font-semibold>
Providers
</div>
</h1>
</div>
<div grid="~ cols-3 gap-2">
<div
v-for="provider in providersList" :key="provider.id"
bg="neutral-300/50 dark:neutral-600" w-full of-hidden rounded-xl
flex="~ col 1"
>
<RouterLink
:to="provider.to"
bg="neutral-100 dark:neutral-800"
hover="bg-neutral-200 dark:bg-neutral-700"
transition="all ease-in-out duration-250"
h-full w-full flex items-center gap-1.5 rounded-lg px-4 py-5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
>
<div flex="~ col 1" gap-1.5>
<div
:class="provider.icon" class="provider-icon size-10"
transition="filter duration-250 ease-in-out"
/>
<div>
{{ provider.name }}
</div>
</div>
</RouterLink>
<div p-2>
<div v-if="provider.configured" size-3 bg="green-500 dark:green-600" rounded-full />
<div v-else size-3 bg="neutral-400 dark:neutral-500" rounded-full />
</div>
</div>
</div>
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lucide:brain translate-x-10 translate-y-10 />
</div>
</template>
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { Collapsable } from '@proj-airi/stage-ui/components'
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
import { useToggle } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const apiKey = ref(providers.value['openrouter-ai']?.apiKey || '')
const baseUrl = ref(providers.value['openrouter-ai']?.baseUrl || '')
const advancedVisible = ref(false)
const toggleAdvancedVisible = useToggle(advancedVisible)
onMounted(() => {
if (!providers.value['openrouter-ai']) {
providers.value['openrouter-ai'] = {
baseUrl: 'https://openrouter.ai/api/v1/',
}
}
})
watch([apiKey, baseUrl], () => {
providers.value['openrouter-ai'] = {
apiKey: apiKey.value,
baseUrl: baseUrl.value || 'https://openrouter.ai/api/v1/',
}
})
</script>
<template>
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-800">Provider</span>
</div>
<div text-3xl font-semibold>
OpenRouter
</div>
</h1>
</div>
<form flex="~ col gap-4" @submit.prevent="() => {}">
<div>
<h2 text-2xl>
<span>Basic</span>
</h2>
</div>
<div>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
API Key
<span class="text-red-500">*</span>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400" text-nowrap>
API Key for OpenRouter
</div>
</div>
<input
v-model="apiKey" type="password"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="sk-or-..."
>
</label>
</div>
<Collapsable w-full>
<template #trigger="slotProps">
<button
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="() => slotProps.setVisible(!slotProps.visible) && toggleAdvancedVisible()"
>
<h2 text-2xl>
<span>Advanced</span>
</h2>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div mt-4>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
Base URL
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
Custom base URL (optional)
</div>
</div>
<input
v-model="baseUrl" type="text"
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
w-full rounded px-2 py-1 text-nowrap text-sm outline-none
placeholder="https://openrouter.ai/api/v1/"
>
</label>
</div>
</Collapsable>
</form>
<div fixed bottom-0 right-0 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lobe-icons:openrouter translate-x-10 translate-y-10 />
</div>
</template>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div flex="~ row" items-center gap-2>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-2xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-800">Settings</span>
</div>
<div text-3xl font-semibold>
Themes
</div>
</h1>
</div>
<div fixed bottom-0 right-0 z--1 text="neutral-100/80 dark:neutral-500/20">
<div text="40" i-lucide:paintbrush translate-x-10 translate-y-10 />
</div>
</template>
<route lang="yaml">
meta:
stageTransition:
name: slide
</route>
+16
View File
@@ -1,5 +1,6 @@
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const useProvidersStore = defineStore('providers', () => {
const providers = useLocalStorage<Record<string, Record<string, unknown>>>('settings/credentials/providers', {})
@@ -7,9 +8,24 @@ export const useProvidersStore = defineStore('providers', () => {
const coreControllerProvider = useLocalStorage<string>('settings/credentials/coreControllerProvider', 'openai')
const audioSynthesisProvider = useLocalStorage<string>('settings/credentials/audioSynthesisProvider', 'elevenlabs')
const configuredForOpenRouter = computed<boolean>(() => {
return !!providers.value['openrouter-ai'].baseUrl && !!providers.value['openrouter-ai'].apiKey
})
function configuredFor(provider: string): boolean {
switch (provider) {
case 'openrouter-ai':
return configuredForOpenRouter.value
default:
return false
}
}
return {
providers,
coreControllerProvider,
audioSynthesisProvider,
configuredForOpenRouter,
configuredFor,
}
})
+10
View File
@@ -680,6 +680,9 @@ importers:
'@iconify-json/svg-spinners':
specifier: ^1.2.2
version: 1.2.2
'@iconify-json/vscode-icons':
specifier: ^1.2.16
version: 1.2.16
'@iconify/utils':
specifier: ^2.3.0
version: 2.3.0
@@ -3093,6 +3096,9 @@ packages:
'@iconify-json/svg-spinners@1.2.2':
resolution: {integrity: sha512-DIErwfBWWzLfmAG2oQnbUOSqZhDxlXvr8941itMCrxQoMB0Hiv8Ww6Bln/zIgxwjDvSem2dKJtap+yKKwsB/2A==}
'@iconify-json/vscode-icons@1.2.16':
resolution: {integrity: sha512-hstc2yVq2UJ6v6FrgjftzXRvphGZBsKxvSeXoFLP1Hgx89TPZKrGE5SV6vqsoeIlLYaQ7OZbXmAoVGroTfGmVQ==}
'@iconify/tools@4.1.1':
resolution: {integrity: sha512-Hybu/HGhv6T8nLQhiG9rKx+ekF7NNpPOEQAy7JRSKht3s3dcFSsPccYzk24Znc9MTxrR6Gak3cg6CPP5dyvS2Q==}
@@ -13732,6 +13738,10 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/vscode-icons@1.2.16':
dependencies:
'@iconify/types': 2.0.0
'@iconify/tools@4.1.1':
dependencies:
'@iconify/types': 2.0.0