feat(stage-web): better settings
Signed-off-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
@@ -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 |
@@ -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>
|
||||
Reference in New Issue
Block a user