fix&style(stage-web|stage-tamagotchi): animation & issue
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
<script setup lang="ts">
|
||||
import type { AiriCard } from '@proj-airi/stage-ui/stores'
|
||||
|
||||
import { Button } from '@proj-airi/stage-ui/components'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
} from 'radix-vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DeleteCardDialog from './DeleteCardDialog.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
cardId: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const cardStore = useAiriCardStore()
|
||||
const { removeCard } = cardStore
|
||||
const { activeCardId } = storeToRefs(cardStore)
|
||||
|
||||
// Get selected card data
|
||||
const selectedCard = computed<AiriCard | undefined>(() => {
|
||||
if (!props.cardId)
|
||||
return undefined
|
||||
return cardStore.getCard(props.cardId)
|
||||
})
|
||||
|
||||
// Get module settings
|
||||
const moduleSettings = computed(() => {
|
||||
if (!selectedCard.value || !selectedCard.value.extensions?.airi?.modules) {
|
||||
return {
|
||||
consciousness: '',
|
||||
speech: '',
|
||||
voice: '',
|
||||
}
|
||||
}
|
||||
|
||||
const airiExt = selectedCard.value.extensions.airi.modules
|
||||
return {
|
||||
consciousness: airiExt.consciousness?.model || '',
|
||||
speech: airiExt.speech?.model || '',
|
||||
voice: airiExt.speech?.voice_id || '',
|
||||
}
|
||||
})
|
||||
|
||||
// Get character settings
|
||||
const characterSettings = computed(() => {
|
||||
if (!selectedCard.value)
|
||||
return {}
|
||||
|
||||
return {
|
||||
personality: selectedCard.value.personality,
|
||||
scenario: selectedCard.value.scenario,
|
||||
systemPrompt: selectedCard.value.systemPrompt,
|
||||
postHistoryInstructions: selectedCard.value.postHistoryInstructions,
|
||||
}
|
||||
})
|
||||
|
||||
// Check if card is active
|
||||
const isActive = computed(() => props.cardId === activeCardId.value)
|
||||
|
||||
// Animation control for card activation
|
||||
const isActivating = ref(false)
|
||||
|
||||
function handleActivate() {
|
||||
isActivating.value = true
|
||||
setTimeout(() => {
|
||||
activeCardId.value = props.cardId
|
||||
isActivating.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function hightlightTagToHtml(text: string) {
|
||||
return text?.replace(/\{\{(.*?)\}\}/g, '<span class="bg-primary-500/20 inline-block">{{ $1 }}</span>').trim()
|
||||
}
|
||||
|
||||
// Delete confirmation
|
||||
const showDeleteConfirm = ref(false)
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
if (selectedCard.value) {
|
||||
removeCard(props.cardId)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
showDeleteConfirm.value = false
|
||||
}
|
||||
|
||||
// Tab type definition
|
||||
interface Tab {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// Active tab ID state
|
||||
const activeTabId = ref('')
|
||||
|
||||
// Tabs for card details
|
||||
const tabs = computed<Tab[]>(() => {
|
||||
const availableTabs: Tab[] = []
|
||||
|
||||
// Description tab - always show if there's description
|
||||
if (selectedCard.value?.description) {
|
||||
availableTabs.push({
|
||||
id: 'description',
|
||||
label: t('settings.pages.card.description_label'),
|
||||
icon: 'i-solar:document-text-linear',
|
||||
})
|
||||
}
|
||||
|
||||
// Notes tab - only show if there are creator notes
|
||||
if (selectedCard.value?.notes) {
|
||||
availableTabs.push({
|
||||
id: 'notes',
|
||||
label: t('settings.pages.card.creator_notes'),
|
||||
icon: 'i-solar:notes-linear',
|
||||
})
|
||||
}
|
||||
|
||||
// Character tab - only show if there are character settings
|
||||
if (Object.values(characterSettings.value).some(value => !!value)) {
|
||||
availableTabs.push({
|
||||
id: 'character',
|
||||
label: t('settings.pages.card.character'),
|
||||
icon: 'i-solar:user-rounded-linear',
|
||||
})
|
||||
}
|
||||
|
||||
// Modules tab - always show
|
||||
availableTabs.push({
|
||||
id: 'modules',
|
||||
label: t('settings.pages.card.modules'),
|
||||
icon: 'i-solar:tuning-square-linear',
|
||||
})
|
||||
|
||||
return availableTabs
|
||||
})
|
||||
|
||||
// Active tab state - set to first available tab by default
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
// If current active tab is not in available tabs, reset to first tab
|
||||
if (!tabs.value.find(tab => tab.id === activeTabId.value))
|
||||
return tabs.value[0]?.id || ''
|
||||
return activeTabId.value
|
||||
},
|
||||
set: (value: string) => {
|
||||
activeTabId.value = value
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="modelValue" @update:open="emit('update:modelValue', $event)">
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="data-[state=open]:animate-fadeIn data-[state=closed]:animate-fadeOut fixed inset-0 z-50 bg-black/50 backdrop-blur-sm" />
|
||||
<DialogContent class="data-[state=open]:animate-contentShow data-[state=closed]:animate-contentHide fixed left-1/2 top-1/2 z-50 m-0 max-h-[90vh] max-w-6xl w-[92vw] flex flex-col overflow-auto border border-neutral-200 rounded-xl bg-white p-5 shadow-xl 2xl:w-[60vw] lg:w-[80vw] md:w-[85vw] xl:w-[70vw] -translate-x-1/2 -translate-y-1/2 dark:border-neutral-700 dark:bg-neutral-800 sm:p-6">
|
||||
<div v-if="selectedCard" class="w-full flex flex-col gap-5">
|
||||
<!-- Header with status indicator -->
|
||||
<div flex="~ col" gap-3>
|
||||
<div flex="~ row" items-center justify-between>
|
||||
<div>
|
||||
<div flex="~ row" items-center gap-2>
|
||||
<DialogTitle text-2xl font-bold class="from-primary-500 to-primary-400 bg-gradient-to-r bg-clip-text text-transparent">
|
||||
{{ selectedCard.name }}
|
||||
</DialogTitle>
|
||||
<div v-if="isActive" class="dark:text-primary-400 bg-primary-100 text-primary-600 dark:bg-primary-900/40 flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium">
|
||||
<div i-solar:check-circle-bold-duotone text-xs />
|
||||
{{ t('settings.pages.card.active_badge') }}
|
||||
</div>
|
||||
</div>
|
||||
<div mt-1 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
v{{ selectedCard.version }}
|
||||
<template v-if="selectedCard.creator">
|
||||
· {{ t('settings.pages.card.created_by') }} <span font-medium>{{ selectedCard.creator }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div flex="~ row" gap-2>
|
||||
<!-- Activation button -->
|
||||
<Button
|
||||
variant="primary"
|
||||
:icon="isActive ? 'i-solar:check-circle-bold-duotone' : 'i-solar:play-circle-broken'"
|
||||
:label="isActive ? t('settings.pages.card.active') : t('settings.pages.card.activate')"
|
||||
:disabled="isActive"
|
||||
:class="{ 'animate-pulse': isActivating }"
|
||||
@click="handleActivate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card content tabs -->
|
||||
<div class="mt-4">
|
||||
<div class="border-b border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex justify-center -mb-px sm:justify-start space-x-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="px-4 py-2 text-sm font-medium"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500 dark:border-primary-400'
|
||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300',
|
||||
]"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div :class="tab.icon" />
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator notes -->
|
||||
<div v-if="activeTab === 'notes' && selectedCard.notes">
|
||||
<div
|
||||
bg="white/60 dark:black/30"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
max-h-60 overflow-auto whitespace-pre-line rounded-lg p-4 text-neutral-700 sm:max-h-80 dark:text-neutral-300 transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
v-html="hightlightTagToHtml(selectedCard.notes)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description section -->
|
||||
<div v-if="activeTab === 'description' && selectedCard.description">
|
||||
<div
|
||||
bg="white/60 dark:black/30"
|
||||
max-h-60 overflow-auto whitespace-pre-line rounded-lg p-4 sm:max-h-80
|
||||
text="neutral-600 dark:neutral-300"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
v-html="hightlightTagToHtml(selectedCard.description)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Character -->
|
||||
<div v-if="activeTab === 'character' && Object.values(characterSettings).some(value => !!value)">
|
||||
<div flex="~ col" max-h-60 gap-4 overflow-auto pr-1 sm:max-h-80>
|
||||
<template v-for="(value, key) in characterSettings" :key="key">
|
||||
<div v-if="value" flex="~ col" gap-2>
|
||||
<h2 text-lg text-neutral-500 font-medium dark:text-neutral-400>
|
||||
{{ t(`settings.pages.card.${key.toLowerCase()}`) }}
|
||||
</h2>
|
||||
<div
|
||||
bg="white/60 dark:black/30"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
max-h-none overflow-auto whitespace-pre-line rounded-lg p-3 text-neutral-700 dark:text-neutral-300
|
||||
v-html="hightlightTagToHtml(value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modules -->
|
||||
<div v-if="activeTab === 'modules'">
|
||||
<div grid="~ cols-1 sm:cols-3" gap-4>
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="white/60 dark:black/30"
|
||||
gap-1 rounded-lg p-3
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
>
|
||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
<div i-lucide:ghost />
|
||||
{{ t('settings.pages.card.consciousness.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ moduleSettings.consciousness ?? 'default' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="white/60 dark:black/30"
|
||||
gap-2 rounded-lg p-3
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
>
|
||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
<div i-lucide:mic />
|
||||
{{ t('settings.pages.card.speech.model') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ moduleSettings.speech ?? 'default' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="white/60 dark:black/30"
|
||||
gap-2 rounded-lg p-3
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
transition="all duration-200"
|
||||
hover="bg-white/80 dark:bg-black/40"
|
||||
>
|
||||
<span flex="~ row" items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400>
|
||||
<div i-lucide:music />
|
||||
{{ t('settings.pages.card.speech.voice') }}
|
||||
</span>
|
||||
<div truncate font-medium>
|
||||
{{ moduleSettings.voice ?? 'default' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
bg="neutral-50/50 dark:neutral-900/50"
|
||||
rounded-xl p-8 text-center
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
shadow="sm"
|
||||
>
|
||||
<div i-solar:card-search-broken mx-auto mb-3 text-6xl text-neutral-400 />
|
||||
{{ t('settings.pages.card.card_not_found') }}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<DeleteCardDialog
|
||||
v-model="showDeleteConfirm"
|
||||
:card-name="selectedCard?.name"
|
||||
@confirm="handleDeleteConfirm"
|
||||
@cancel="showDeleteConfirm = false"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
isActive: boolean
|
||||
isSelected: boolean
|
||||
version: string
|
||||
consciousnessModel: string
|
||||
voiceModel: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'select'): void
|
||||
(e: 'activate'): void
|
||||
(e: 'delete'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
relative min-h-120px flex="~ col" cursor-pointer overflow-hidden rounded-xl
|
||||
:class="[
|
||||
isSelected
|
||||
? 'border-2 border-primary-400 dark:border-primary-600'
|
||||
: 'border-2 border-neutral-100 dark:border-neutral-800/25',
|
||||
]"
|
||||
bg="neutral-200/50 dark:neutral-800/50"
|
||||
drop-shadow="none hover:[0px_4px_4px_rgba(220,220,220,0.4)] active:[0px_0px_0px_rgba(220,220,220,0.25)] dark:hover:none"
|
||||
transition="all ease-in-out duration-400"
|
||||
before="content-empty absolute inset-0 z-0 w-25% h-full transition-all duration-400 ease-in-out bg-gradient-to-r from-primary-500/0 to-primary-500/0 dark:from-primary-400/0 dark:to-primary-400/0 mask-image-[linear-gradient(120deg,white_100%)] opacity-0"
|
||||
hover="before:(w-50% opacity-100 bg-gradient-to-r from-primary-500/20 via-primary-500/10 to-transparent dark:from-primary-400/20 dark:via-primary-400/10 dark:to-transparent)"
|
||||
@click="emit('select')"
|
||||
>
|
||||
<!-- Card content -->
|
||||
<div
|
||||
relative flex="~ col 1" justify-between gap-3 overflow-hidden rounded-lg bg="white dark:neutral-900" p-5
|
||||
transition="all ease-in-out duration-400"
|
||||
after="content-empty absolute inset-0 z--2 w-full h-full bg-dotted-[neutral-200/80] bg-size-10px mask-image-[linear-gradient(165deg,white_30%,transparent_50%)] transition-all duration-400 ease-in-out"
|
||||
hover="after:bg-dotted-[primary-300/50] dark:after:bg-dotted-[primary-200/20]"
|
||||
>
|
||||
<!-- Card header (name and badge) -->
|
||||
<div z-1 flex items-start justify-between gap-2>
|
||||
<h3 flex-1 truncate text-lg font-bold hover="text-primary-600 dark:text-primary-300">
|
||||
{{ name }}
|
||||
</h3>
|
||||
<div v-if="isActive" shrink-0 rounded-md p-1 bg="primary-100 dark:primary-900/40" text="primary-600 dark:primary-400">
|
||||
<div i-solar:check-circle-bold-duotone text-sm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card description -->
|
||||
<p v-if="description" line-clamp-3 min-h-40px flex-1 text-sm text="neutral-500 dark:neutral-400" hover="text-primary-600/80 dark:text-primary-300/80">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<!-- Card stats -->
|
||||
<div z-1 flex items-center justify-between text-xs text="neutral-500 dark:neutral-400">
|
||||
<div>v{{ version }}</div>
|
||||
<div flex items-center gap-1.5>
|
||||
<div flex items-center gap-0.5>
|
||||
<div i-lucide:ghost text-xs />
|
||||
<span>{{ consciousnessModel }}</span>
|
||||
</div>
|
||||
<div flex items-center gap-0.5>
|
||||
<div i-lucide:mic text-xs />
|
||||
<span>{{ voiceModel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card actions -->
|
||||
<div flex items-center justify-end px-2 py-1.5>
|
||||
<button
|
||||
rounded-lg p-1.5 transition-colors hover="bg-neutral-200 dark:bg-neutral-700/50"
|
||||
:disabled="isActive"
|
||||
@click.stop="emit('activate')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
isActive
|
||||
? 'i-solar:check-circle-bold-duotone text-primary-500 dark:text-primary-400'
|
||||
: 'i-solar:play-circle-broken text-neutral-500 dark:text-neutral-400',
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="id !== 'default'"
|
||||
rounded-lg p-1.5 transition-colors hover="bg-neutral-200 dark:bg-neutral-700/50"
|
||||
@click.stop="emit('delete')"
|
||||
>
|
||||
<div i-solar:trash-bin-trash-linear text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@proj-airi/stage-ui/components/Button'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
} from 'radix-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
cardName?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:modelValue', false)
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
emit('update:modelValue', false)
|
||||
emit('confirm')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot :open="modelValue" @update:open="emit('update:modelValue', $event)">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="data-[state=open]:animate-fadeIn data-[state=closed]:animate-fadeOut fixed inset-0 z-50 bg-black/50" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow data-[state=closed]:animate-contentHide fixed left-1/2 top-1/2 z-50 max-w-md w-full border border-neutral-200 rounded-xl bg-white p-6 shadow-xl -translate-x-1/2 -translate-y-1/2 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<AlertDialogTitle class="mb-4 text-xl font-bold">
|
||||
{{ t('settings.pages.card.delete_card') }}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mb-6">
|
||||
{{ t('settings.pages.card.delete_confirmation') }} <b>"{{ cardName || '' }}"</b>
|
||||
</AlertDialogDescription>
|
||||
|
||||
<div class="flex flex-row justify-end gap-3">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button
|
||||
variant="secondary"
|
||||
:label="t('settings.pages.card.cancel')"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button
|
||||
variant="danger"
|
||||
:label="t('settings.pages.card.delete')"
|
||||
@click="handleConfirm"
|
||||
/>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,339 @@
|
||||
<script setup lang="ts">
|
||||
import type { ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import CardDetailDialog from './components/CardDetailDialog.vue'
|
||||
import CardListItem from './components/CardListItem.vue'
|
||||
import DeleteCardDialog from './components/DeleteCardDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const cardStore = useAiriCardStore()
|
||||
const { addCard, removeCard } = cardStore
|
||||
const { cards, activeCardId } = storeToRefs(cardStore)
|
||||
|
||||
// Currently selected card ID (different from active card ID)
|
||||
const selectedCardId = ref<string>('')
|
||||
// Dialog state
|
||||
const isCardDialogOpen = ref(false)
|
||||
|
||||
// Search query
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Sort option
|
||||
const sortOption = ref('nameAsc')
|
||||
|
||||
// Drag and drop
|
||||
const isDragging = ref(false)
|
||||
|
||||
// Card list data structure
|
||||
interface CardItem {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
deprecated?: boolean
|
||||
customizable?: boolean
|
||||
}
|
||||
|
||||
// Transform cards Map to array for display
|
||||
const cardsArray = computed<CardItem[]>(() =>
|
||||
Array.from(cards.value.entries()).map(([id, card]) => ({
|
||||
id,
|
||||
name: card.name,
|
||||
description: card.description,
|
||||
})),
|
||||
)
|
||||
|
||||
// Filtered cards based on search query
|
||||
const filteredCards = computed<CardItem[]>(() => {
|
||||
if (!searchQuery.value)
|
||||
return cardsArray.value
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return cardsArray.value.filter(item =>
|
||||
item.name.toLowerCase().includes(query)
|
||||
|| (item.description && item.description.toLowerCase().includes(query)),
|
||||
)
|
||||
})
|
||||
|
||||
// Sorted filtered cards based on sort option
|
||||
const sortedFilteredCards = computed<CardItem[]>(() => {
|
||||
// Create a new array to avoid mutating the source
|
||||
const sorted = [...filteredCards.value]
|
||||
|
||||
if (sortOption.value === 'nameAsc')
|
||||
return sorted.sort((a, b) => a.name.localeCompare(b.name))
|
||||
else if (sortOption.value === 'nameDesc')
|
||||
return sorted.sort((a, b) => b.name.localeCompare(a.name))
|
||||
else if (sortOption.value === 'recent')
|
||||
return sorted.sort((a, b) => b.id.localeCompare(a.id))
|
||||
else
|
||||
return sorted
|
||||
})
|
||||
|
||||
// Delete confirmation
|
||||
const showDeleteConfirm = ref(false)
|
||||
const cardToDelete = ref<string | null>(null)
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
if (cardToDelete.value) {
|
||||
removeCard(cardToDelete.value)
|
||||
cardToDelete.value = null
|
||||
showDeleteConfirm.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Card deletion confirmation
|
||||
function confirmDelete(id: string) {
|
||||
cardToDelete.value = id
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles card file upload and processing
|
||||
*/
|
||||
async function handleUpload() {
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.json'
|
||||
|
||||
fileInput.onchange = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file)
|
||||
return
|
||||
|
||||
try {
|
||||
const content = await file.text()
|
||||
const cardJSON = JSON.parse(content) as ccv3.CharacterCardV3
|
||||
|
||||
// Add card and select it
|
||||
selectedCardId.value = addCard(cardJSON)
|
||||
isCardDialogOpen.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error processing card file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fileInput.click()
|
||||
}
|
||||
|
||||
function handleSelectCard(cardId: string) {
|
||||
selectedCardId.value = cardId
|
||||
isCardDialogOpen.value = true
|
||||
}
|
||||
|
||||
// Card activation
|
||||
function activateCard(id: string) {
|
||||
activeCardId.value = id
|
||||
}
|
||||
|
||||
// Card version number
|
||||
function getVersionNumber(id: string) {
|
||||
const card = cards.value.get(id)
|
||||
return card?.version || '1.0.0'
|
||||
}
|
||||
|
||||
// Card module short name
|
||||
function getModuleShortName(id: string, module: 'consciousness' | 'voice') {
|
||||
const card = cards.value.get(id)
|
||||
if (!card || !card.extensions?.airi?.modules)
|
||||
return 'default'
|
||||
|
||||
const airiExt = card.extensions.airi.modules
|
||||
|
||||
if (module === 'consciousness') {
|
||||
return airiExt.consciousness?.model ? airiExt.consciousness.model.split('-').pop() || 'default' : 'default'
|
||||
}
|
||||
else if (module === 'voice') {
|
||||
return airiExt.speech?.voice_id || 'default'
|
||||
}
|
||||
|
||||
return 'default'
|
||||
}
|
||||
</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="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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.card.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div rounded-xl p-4 flex="~ col gap-4">
|
||||
<!-- Toolbar with search and filters -->
|
||||
<div flex="~ row" flex-wrap items-center justify-between gap-4>
|
||||
<!-- Search bar -->
|
||||
<div class="relative min-w-[200px] flex-1" inline-flex="~" w-full items-center>
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<div i-solar:magnifer-line-duotone class="text-neutral-500 dark:text-neutral-400" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
class="w-full rounded-xl p-2.5 pl-10 text-sm outline-none"
|
||||
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
transition="all duration-200 ease-in-out"
|
||||
bg="white dark:neutral-900"
|
||||
:placeholder="t('settings.pages.card.search')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Sort options -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div text-sm text-neutral-500 dark:text-neutral-400>
|
||||
{{ t('settings.pages.card.sort_by') }}:
|
||||
</div>
|
||||
<select
|
||||
v-model="sortOption"
|
||||
class="rounded-lg p-1.5 text-sm outline-none"
|
||||
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
|
||||
bg="white dark:neutral-900"
|
||||
>
|
||||
<option value="nameAsc">
|
||||
{{ t('settings.pages.card.name_asc') }}
|
||||
</option>
|
||||
<option value="nameDesc">
|
||||
{{ t('settings.pages.card.name_desc') }}
|
||||
</option>
|
||||
<option value="recent">
|
||||
{{ t('settings.pages.card.recent') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Masonry card layout -->
|
||||
<div
|
||||
class="mt-4"
|
||||
:class="{ 'grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 grid-auto-rows-[minmax(min-content,max-content)] grid-auto-flow-dense sm:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] sm:gap-5 md:grid-cols-[repeat(auto-fill,minmax(220px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(250px,1fr))]': cards.size > 0 }"
|
||||
>
|
||||
<!-- Upload card -->
|
||||
<div
|
||||
class="relative min-h-[120px] flex flex-col cursor-pointer items-center justify-center border-2 rounded-xl border-dashed p-6 transition-all duration-300"
|
||||
border="neutral-200 dark:neutral-700 hover:primary-300 dark:hover:primary-700"
|
||||
bg="white/60 dark:black/30 hover:white/80 dark:hover:black/40"
|
||||
:style="{ transform: 'scale(0.98)', opacity: 0.95 }"
|
||||
hover="scale-100 opacity-100 shadow-md dark:shadow-xl"
|
||||
@click="handleUpload"
|
||||
>
|
||||
<div i-solar:upload-square-line-duotone mb-4 text-5xl text="neutral-400 dark:neutral-500" />
|
||||
<p font-medium text="center neutral-600 dark:neutral-300">
|
||||
{{ t('settings.pages.card.upload') }}
|
||||
</p>
|
||||
<p text="center neutral-500 dark:neutral-400" mt-2 text-sm>
|
||||
{{ t('settings.pages.card.upload_desc') }}
|
||||
</p>
|
||||
|
||||
<!-- Drag overlay -->
|
||||
<div
|
||||
v-if="isDragging"
|
||||
class="bg-primary-100/80 border-primary-400 dark:bg-primary-900/80 dark:border-primary-600 absolute inset-0 flex items-center justify-center border-2 rounded-xl"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div i-solar:upload-minimalistic-bold class="text-primary-500 dark:text-primary-400 mb-2 text-5xl" />
|
||||
<p font-medium text="primary-600 dark:primary-300">
|
||||
{{ t('settings.pages.card.drop_here') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Items -->
|
||||
<template v-if="cards.size > 0">
|
||||
<CardListItem
|
||||
v-for="item in sortedFilteredCards"
|
||||
:id="item.id"
|
||||
:key="item.id"
|
||||
:name="item.name"
|
||||
:description="item.description"
|
||||
:is-active="item.id === activeCardId"
|
||||
:is-selected="item.id === selectedCardId && isCardDialogOpen"
|
||||
:version="getVersionNumber(item.id)"
|
||||
:consciousness-model="getModuleShortName(item.id, 'consciousness')"
|
||||
:voice-model="getModuleShortName(item.id, 'voice')"
|
||||
@select="handleSelectCard(item.id)"
|
||||
@activate="activateCard(item.id)"
|
||||
@delete="confirmDelete(item.id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- No cards message -->
|
||||
<div
|
||||
v-if="cards.size === 0"
|
||||
class="col-span-full rounded-xl p-8 text-center"
|
||||
border="~ neutral-200/50 dark:neutral-700/30"
|
||||
bg="neutral-50/50 dark:neutral-900/50"
|
||||
>
|
||||
<div i-solar:card-search-broken mx-auto mb-3 text-6xl text-neutral-400 />
|
||||
<p>{{ t('settings.pages.card.no_cards') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- No search results -->
|
||||
<div
|
||||
v-if="searchQuery && sortedFilteredCards.length === 0"
|
||||
class="col-span-full flex items-center gap-3 border-2 border-amber-200 rounded-xl bg-amber-50/80 p-4 dark:border-amber-800 dark:bg-amber-900/30"
|
||||
>
|
||||
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ t('settings.pages.card.no_results') }}</span>
|
||||
<span class="text-sm text-amber-600 dark:text-amber-400">
|
||||
{{ t('settings.pages.card.try_different_search') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<DeleteCardDialog
|
||||
v-model="showDeleteConfirm"
|
||||
:card-name="cardToDelete ? cardStore.getCard(cardToDelete)?.name : ''"
|
||||
@confirm="handleDeleteConfirm"
|
||||
@cancel="cardToDelete = null"
|
||||
/>
|
||||
|
||||
<!-- Card detail dialog -->
|
||||
<CardDetailDialog
|
||||
v-model="isCardDialogOpen"
|
||||
:card-id="selectedCardId"
|
||||
/>
|
||||
|
||||
<!-- Background decoration -->
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[72dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:id-card />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
[],
|
||||
["#A5978B", "#D8CAAF", "#B8B4A7", "#C4BCB1", "#E5DED8", "#9A8F7D", "#BEB5A7", "#C9C0B6"],
|
||||
["#7A9EAF", "#B8C7CC", "#D4B79C", "#8B9D77", "#C7D5CB", "#E6D0B1", "#94A7B1", "#B4C8C3"],
|
||||
["#D9B48F", "#B5917A", "#8C7A6B", "#A17F5F", "#B98C46", "#C7A252", "#DAB300", "#D19826"],
|
||||
["#9BA7B0", "#C1CBD4", "#A5ADB6", "#8B959E", "#D4DCE4", "#7F8A94", "#B3BCC6", "#98A4AE"],
|
||||
["#E4C6D0", "#A61B29", "#5D513C", "#789262", "#1C0D1A", "#F7C242", "#62A9DD", "#8C4B3C"]
|
||||
]
|
||||
@@ -0,0 +1,347 @@
|
||||
<script setup lang="ts">
|
||||
import { Section } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import CheckBar from '../../../components/Settings/CheckBar.vue'
|
||||
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
import COLOR_PRESETS from './color-presets.json'
|
||||
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
const dark = useDark()
|
||||
const { t } = useI18n()
|
||||
|
||||
const usePageSpecificTransitionsSettingChanged = ref(false)
|
||||
|
||||
const { iconAnimationStarted, showIconAnimation, animationIcon } = useIconAnimation('i-lucide:paintbrush')
|
||||
|
||||
// avoid showing the animation component when the page specific transitions are enabled
|
||||
watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions], () => {
|
||||
usePageSpecificTransitionsSettingChanged.value = true
|
||||
})
|
||||
</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="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" text-nowrap>{{ $t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ $t('settings.pages.themes.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Section
|
||||
v-motion
|
||||
:title="t('settings.sections.section.general.title')"
|
||||
icon="i-solar:filters-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (1 * 10)"
|
||||
:delay="1 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<!-- Theme Setting -->
|
||||
<CheckBar
|
||||
v-model="dark"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (2 * 10)"
|
||||
:delay="2 * 50"
|
||||
icon-on="i-solar:moon-stars-bold-duotone"
|
||||
icon-off="i-solar:sun-fog-bold-duotone"
|
||||
text="settings.theme"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<!-- Language Setting -->
|
||||
<div
|
||||
v-motion
|
||||
class="w-full flex items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (3 * 10)"
|
||||
:delay="3 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
{{ $t('settings.language.title') }}
|
||||
<select
|
||||
v-model="settings.language"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer bg-transparent text-right outline-none
|
||||
>
|
||||
<option value="en-US">
|
||||
{{ $t('settings.language.english') }}
|
||||
</option>
|
||||
<option value="zh-CN">
|
||||
{{ $t('settings.language.chinese') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
v-motion
|
||||
:title="t('settings.pages.themes.sections.section.custom-color.title')"
|
||||
icon="i-solar:pallete-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (4 * 10)"
|
||||
:delay="4 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div
|
||||
v-motion flex items-center
|
||||
justify-between
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (5 * 10)"
|
||||
:delay="5 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span text-lg font-semibold>{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}</span>
|
||||
<label relative flex cursor-pointer items-center gap-2>
|
||||
<input
|
||||
v-model="settings.themeColorsHueDynamic"
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
>
|
||||
<div
|
||||
class="peer-checked:bg-primary-500 h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
|
||||
/>
|
||||
{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
v-model="settings.themeColorsHue"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (6 * 10)"
|
||||
:delay="6 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
type="range" min="0" max="360" step="0.01" class="theme-hue-slider"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
:class="settings.themeColorsHueDynamic ? 'opacity-25 cursor-not-allowed' : 'cursor-pointer'"
|
||||
>
|
||||
<div
|
||||
v-motion
|
||||
class="color-bar"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (7 * 10)"
|
||||
:delay="7 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg-primary-50>50</span>
|
||||
<span bg-primary-100>100</span>
|
||||
<span bg-primary-200>200</span>
|
||||
<span bg-primary-300>300</span>
|
||||
<span bg-primary-400>400</span>
|
||||
<span bg-primary-500>500</span>
|
||||
<div
|
||||
v-motion
|
||||
text-white
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (8 * 10)"
|
||||
:delay="8 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg-primary-600>600</span>
|
||||
<span bg-primary-700>700</span>
|
||||
<span bg-primary-800>800</span>
|
||||
<span bg-primary-900>900</span>
|
||||
<span bg-primary-950>950</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-motion
|
||||
class="color-bar transparency-grid"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (9 * 10)"
|
||||
:delay="9 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg="primary-500/5">500/5</span>
|
||||
<span bg="primary-500/10">500/10</span>
|
||||
<span bg="primary-500/20">500/20</span>
|
||||
<span bg="primary-500/30">500/30</span>
|
||||
<span bg="primary-500/40">500/40</span>
|
||||
<span bg="primary-500/50">500/50</span>
|
||||
<span bg="primary-500/60">500/60</span>
|
||||
<span bg="primary-500/70">500/70</span>
|
||||
<span bg="primary-500/80">500/80</span>
|
||||
<span bg="primary-500/90">500/90</span>
|
||||
<span bg="primary-500">500</span>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
v-motion title="settings.pages.themes.sections.section.theme-presets.title"
|
||||
icon="i-solar:magic-stick-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (10 * 10)"
|
||||
:delay="10 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div
|
||||
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
|
||||
v-motion
|
||||
class="w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none transition-all duration-250 ease-in-out"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (11 * 10) + (i * 10)"
|
||||
:delay="11 * 50 + (i * 50)"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div>
|
||||
<span font-medium>{{ $rt(title) }}</span>
|
||||
<div text="sm neutral-500">
|
||||
{{ $rt(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<ColorPalette :colors="(colors as any[]).map((name, j) => ({ hex: COLOR_PRESETS[i][j], name: $rt(name) }))" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
v-motion title="settings.pages.themes.sections.section.developer.title"
|
||||
icon="i-solar:code-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (18 * 10)"
|
||||
:delay="18 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<CheckBar
|
||||
v-model="settings.disableTransitions"
|
||||
v-motion
|
||||
icon-on="i-solar:people-nearby-bold-duotone"
|
||||
icon-off="i-solar:running-2-line-duotone"
|
||||
text="settings.animations.stage-transitions.title"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (19 * 10)"
|
||||
:delay="19 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="settings.usePageSpecificTransitions"
|
||||
v-motion
|
||||
:disabled="settings.disableTransitions"
|
||||
icon-on="i-solar:running-2-line-duotone"
|
||||
icon-off="i-solar:people-nearby-bold-duotone"
|
||||
text="settings.animations.use-page-specific-transitions.title"
|
||||
description="settings.animations.use-page-specific-transitions.description"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (20 * 10)"
|
||||
:delay="20 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation && !usePageSpecificTransitionsSettingChanged"
|
||||
:z-index="-1"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.theme-hue-slider {
|
||||
--at-apply: appearance-none h-10 rounded-lg;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
oklch(85% 0.2 0),
|
||||
oklch(85% 0.2 60),
|
||||
oklch(85% 0.2 120),
|
||||
oklch(85% 0.2 180),
|
||||
oklch(85% 0.2 240),
|
||||
oklch(85% 0.2 300),
|
||||
oklch(85% 0.2 360)
|
||||
);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
--at-apply: appearance-none w-2 h-12 rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 shadow-md border-2
|
||||
border-white hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
--at-apply: w-2 h-12 rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 shadow-md border-2 border-white border-box
|
||||
hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
}
|
||||
|
||||
.color-bar {
|
||||
--at-apply: flex of-hidden rounded-lg lh-10 text-center text-black;
|
||||
|
||||
* {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.transparency-grid {
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
@@ -1,221 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconAnimation from '../../components/IconAnimation.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const iconAnimationStarted = ref(false)
|
||||
const iconAnimation = ref<InstanceType<typeof IconAnimation>>()
|
||||
const resolveAnimation = ref<() => void>()
|
||||
const { t } = useI18n()
|
||||
const { language, disableTransitions } = storeToRefs(useSettings())
|
||||
const dark = useDark()
|
||||
|
||||
function handleLanguageChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement
|
||||
language.value = target.value
|
||||
const animationIcon = ref('')
|
||||
const animationPosition = ref('')
|
||||
const showAnimationComponent = ref(false)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
function handleAnimationEnded() {
|
||||
resolveAnimation.value?.()
|
||||
}
|
||||
|
||||
async function handleIconItemClick(event: MouseEvent, setting: typeof settings.value[0]) {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const iconElement = target.querySelector('.menu-icon-item-icon') as HTMLElement
|
||||
if (!iconElement)
|
||||
return
|
||||
|
||||
// get the position of the icon element
|
||||
const rect = iconElement.getBoundingClientRect()
|
||||
const position = `${rect.left}px, ${rect.top}px`
|
||||
|
||||
// set the icon and position
|
||||
animationIcon.value = setting.icon
|
||||
animationPosition.value = position
|
||||
|
||||
// show the animation component
|
||||
showAnimationComponent.value = true
|
||||
|
||||
// wait for the DOM to update
|
||||
await nextTick()
|
||||
|
||||
// start the animation
|
||||
iconAnimationStarted.value = true
|
||||
}
|
||||
|
||||
const removeBeforeEach = router.beforeEach(async (_, __, next) => {
|
||||
if (!settingsStore.usePageSpecificTransitions || settingsStore.disableTransitions) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
resolveAnimation.value = resolve
|
||||
})
|
||||
removeBeforeEach()
|
||||
next()
|
||||
})
|
||||
|
||||
const settings = computed(() => [
|
||||
{
|
||||
title: t('settings.pages.card.title'),
|
||||
description: t('settings.pages.card.description'),
|
||||
icon: 'i-lucide:id-card',
|
||||
to: '/settings/airi-card',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.modules.title'),
|
||||
description: t('settings.pages.modules.description'),
|
||||
icon: 'i-lucide:blocks',
|
||||
to: '/settings/modules',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.models.title'),
|
||||
description: t('settings.pages.models.description'),
|
||||
icon: 'i-lucide:person-standing',
|
||||
to: '/settings/models',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.memory.title'),
|
||||
description: t('settings.pages.memory.description'),
|
||||
icon: 'i-lucide:sprout',
|
||||
to: '/settings/memory',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.providers.title'),
|
||||
description: t('settings.pages.providers.description'),
|
||||
icon: 'i-lucide:brain',
|
||||
to: '/settings/providers',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.themes.title'),
|
||||
description: t('settings.pages.themes.description'),
|
||||
icon: 'i-lucide:paintbrush',
|
||||
to: '/settings/appearance',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-motion
|
||||
flex="~ row" items-center
|
||||
gap-2
|
||||
flex items-center gap-2
|
||||
:initial="{ opacity: 0, x: 10 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
:duration="100"
|
||||
>
|
||||
<button @click="router.back()">
|
||||
<div i-solar:alt-arrow-left-line-duotone text-2xl />
|
||||
</button>
|
||||
<h1 text-3xl>
|
||||
{{ t('settings.title') }}
|
||||
{{ $t('settings.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div flex="~ col gap-4">
|
||||
<div flex="~ col gap-4">
|
||||
<IconItem
|
||||
v-for="(setting, index) in settings"
|
||||
:key="setting.to"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:title="t('settings.pages.modules.title')"
|
||||
:description="t('settings.pages.modules.description')"
|
||||
icon="i-lucide:blocks"
|
||||
to="/settings/modules"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="50"
|
||||
:title="t('settings.pages.models.title')"
|
||||
:description="t('settings.pages.models.description')"
|
||||
icon="i-lucide:person-standing"
|
||||
to="/settings/models"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="100"
|
||||
:title="t('settings.pages.providers.title')"
|
||||
:description="t('settings.pages.providers.description')"
|
||||
icon="i-lucide:brain"
|
||||
to="/settings/providers"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
:title="t('settings.pages.themes.title')"
|
||||
:description="t('settings.pages.themes.description')"
|
||||
icon="i-lucide:paintbrush"
|
||||
to="/settings/themes"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
:title="t('settings.pages.shortcuts.title')"
|
||||
:description="t('settings.pages.shortcuts.description')"
|
||||
icon="i-lucide:keyboard"
|
||||
to="/settings/shortcuts"
|
||||
:style="{
|
||||
transitionDelay: `${index * 50}ms`, // delay between each item, unocss doesn't support dynamic generation of classes now
|
||||
}"
|
||||
:title="setting.title"
|
||||
:description="setting.description"
|
||||
:icon="setting.icon"
|
||||
:to="setting.to"
|
||||
@click="(e: Event) => handleIconItemClick(e, setting)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[75dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 45 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="500"
|
||||
>
|
||||
<h2 text-2xl>
|
||||
{{ t('settings.sections.section.general.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Language Setting -->
|
||||
<div
|
||||
v-motion
|
||||
grid="~ cols-[150px_1fr]"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250" items-center gap-1.5 rounded-lg px-4
|
||||
py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="200"
|
||||
>
|
||||
<div text="sm">
|
||||
<span>{{ t('settings.language.title') }}</span>
|
||||
</div>
|
||||
<div flex="~ row" w-full justify-end>
|
||||
<select
|
||||
class="w-32"
|
||||
bg="transparent"
|
||||
text="sm right neutral-800 dark:neutral-100"
|
||||
transition="all ease-in-out duration-250"
|
||||
outline="none"
|
||||
cursor-pointer
|
||||
@change="handleLanguageChange"
|
||||
>
|
||||
<option value="en-US">
|
||||
{{ t('settings.language.english') }}
|
||||
</option>
|
||||
<option value="zh-CN">
|
||||
{{ t('settings.language.chinese') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Theme Setting -->
|
||||
<label
|
||||
v-motion
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex cursor-pointer rounded-lg px-4 py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="250"
|
||||
>
|
||||
<input
|
||||
v-model="dark"
|
||||
text="neutral-800 dark:neutral-100"
|
||||
:checked="dark"
|
||||
:aria-checked="dark"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div flex="~ row" w-full items-center gap-1.5>
|
||||
<div text="sm" w-full flex-1>
|
||||
<span>{{ t('settings.dark-mode') }}</span>
|
||||
</div>
|
||||
<div select-none>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div
|
||||
v-if="dark"
|
||||
i-solar:moon-stars-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
i-solar:sun-fog-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<!-- Developer Settings -->
|
||||
<label
|
||||
v-motion
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex cursor-pointer rounded-lg px-4 py-3
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="300"
|
||||
>
|
||||
<input
|
||||
v-model="disableTransitions"
|
||||
text="neutral-800 dark:neutral-100"
|
||||
:checked="disableTransitions"
|
||||
:aria-checked="disableTransitions"
|
||||
type="checkbox"
|
||||
hidden appearance-none outline-none
|
||||
>
|
||||
<div flex="~ row" w-full items-center gap-1.5>
|
||||
<div text="sm" w-full flex-1>
|
||||
<span>{{ t('settings.stage-transitions') }}</span>
|
||||
</div>
|
||||
<div select-none>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div
|
||||
v-if="disableTransitions"
|
||||
i-solar:people-nearby-bold-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
i-solar:running-2-line-duotone
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div v-motion text="60" i-lucide:cog />
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showAnimationComponent && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions"
|
||||
ref="iconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="6 * 1.2"
|
||||
:position="animationPosition"
|
||||
:duration="1000"
|
||||
text-color="text-neutral-400/50 dark:text-neutral-600/20"
|
||||
:started="iconAnimationStarted"
|
||||
@animation-ended.once="handleAnimationEnded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
// import { useServerStore } from '@proj-airi/stage-ui/stores'
|
||||
// import { storeToRefs } from 'pinia'
|
||||
// import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// const { server } = storeToRefs(useServerStore())
|
||||
|
||||
// onMounted(() => {
|
||||
// server.value?.connect()
|
||||
// })
|
||||
</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="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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.memory.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 40 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:sprout />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Live2DCanvas from '@proj-airi/stage-ui/components/Live2D/Canvas.vue'
|
||||
import Live2DModel from '@proj-airi/stage-ui/components/Live2D/Model.vue'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { Vibrant } from 'node-vibrant/browser'
|
||||
import { ref } from 'vue'
|
||||
@@ -8,12 +9,14 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Live2DSettings from '../../../components/Widgets/Live2DSettings.vue'
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const live2dContainerRef = ref<HTMLDivElement>()
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
const { width, height } = useElementBounding(live2dContainerRef)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
const palette = ref<string[]>([])
|
||||
|
||||
@@ -28,13 +31,22 @@ async function extractColorsFromModel() {
|
||||
}
|
||||
|
||||
const frameUrl = URL.createObjectURL(frame)
|
||||
const vibrant = new Vibrant(frameUrl)
|
||||
try {
|
||||
const vibrant = new Vibrant(frameUrl)
|
||||
|
||||
const paletteFromVibrant = await vibrant.getPalette()
|
||||
palette.value = Object.values(paletteFromVibrant).map(color => color?.hex).filter(it => typeof it === 'string')
|
||||
|
||||
URL.revokeObjectURL(frameUrl)
|
||||
const paletteFromVibrant = await vibrant.getPalette()
|
||||
palette.value = Object.values(paletteFromVibrant).map(color => color?.hex).filter(it => typeof it === 'string')
|
||||
}
|
||||
finally {
|
||||
URL.revokeObjectURL(frameUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-lucide:person-standing')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,14 +58,12 @@ async function extractColorsFromModel() {
|
||||
:leave="{ opacity: 0, x: -10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
<button i-solar:alt-arrow-left-line-duotone text-2xl @click="router.back()" />
|
||||
<h1 relative text-nowrap>
|
||||
<div absolute left-0 top-0 translate-y="[-80%]" text="neutral-300 dark:neutral-500">
|
||||
{{ t('settings.title') }}
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
<div text-3xl font-semibold>
|
||||
{{ t('settings.pages.models.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
@@ -67,7 +77,18 @@ async function extractColorsFromModel() {
|
||||
<Live2DSettings w="50%" h="80vh" :palette="palette" @extract-colors-from-model="extractColorsFromModel" />
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div v-if="!settingsStore.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:person-standing />
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,4 +97,5 @@ async function extractColorsFromModel() {
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -214,7 +214,7 @@ function updateCustomModelName(value: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:ghost />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -28,6 +31,14 @@ const modulesList = computed<Module[]>(() => [
|
||||
to: '/settings/modules/consciousness',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'speech',
|
||||
name: t('settings.pages.modules.speech.title'),
|
||||
description: t('settings.pages.modules.speech.description'),
|
||||
icon: 'i-lucide:mic',
|
||||
to: '/settings/modules/speech',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'hearing',
|
||||
name: t('settings.pages.modules.hearing.title'),
|
||||
@@ -36,6 +47,30 @@ const modulesList = computed<Module[]>(() => [
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'vision',
|
||||
name: t('settings.pages.modules.vision.title'),
|
||||
description: t('settings.pages.modules.vision.description'),
|
||||
icon: 'i-lucide:eye',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'memory-short-term',
|
||||
name: t('settings.pages.modules.memory-short-term.title'),
|
||||
description: t('settings.pages.modules.memory-short-term.description'),
|
||||
icon: 'i-lucide:book',
|
||||
to: '/settings/modules/memory-short-term',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'memory-long-term',
|
||||
name: t('settings.pages.modules.memory-long-term.title'),
|
||||
description: t('settings.pages.modules.memory-long-term.description'),
|
||||
icon: 'i-lucide:book-copy',
|
||||
to: '/settings/modules/memory-long-term',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'messaging-discord',
|
||||
name: t('settings.pages.modules.messaging-discord.title'),
|
||||
@@ -45,34 +80,10 @@ const modulesList = computed<Module[]>(() => [
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'speech',
|
||||
name: t('settings.pages.modules.speech.title'),
|
||||
description: t('settings.pages.modules.speech.description'),
|
||||
icon: 'i-lucide:mic',
|
||||
to: '/settings/modules/speech',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'memory-short-term',
|
||||
name: t('settings.pages.modules.memory-short-term.title'),
|
||||
description: t('settings.pages.modules.memory-short-term.description'),
|
||||
icon: 'i-lucide:book',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'memory-long-term',
|
||||
name: t('settings.pages.modules.memory-long-term.title'),
|
||||
description: t('settings.pages.modules.memory-long-term.description'),
|
||||
icon: 'i-lucide:book-copy',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'vision',
|
||||
name: t('settings.pages.modules.vision.title'),
|
||||
description: t('settings.pages.modules.vision.description'),
|
||||
icon: 'i-lucide:eye',
|
||||
id: 'x',
|
||||
name: t('settings.pages.modules.x.title'),
|
||||
description: t('settings.pages.modules.x.description'),
|
||||
icon: 'i-simple-icons:x',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
@@ -92,15 +103,13 @@ const modulesList = computed<Module[]>(() => [
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
{
|
||||
id: 'x',
|
||||
name: t('settings.pages.modules.x.title'),
|
||||
description: t('settings.pages.modules.x.description'),
|
||||
icon: 'i-simple-icons:x',
|
||||
to: '',
|
||||
configured: false,
|
||||
},
|
||||
])
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-lucide:blocks')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -142,8 +151,26 @@ const modulesList = computed<Module[]>(() => [
|
||||
:configured="module.configured"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:blocks />
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
:z-index="-1"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
/>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[72dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 0 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:blocks />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -151,4 +178,5 @@ const modulesList = computed<Module[]>(() => [
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
// import { useServerStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// const serverStore = useServerStore()
|
||||
</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="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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.modules.memory-long-term.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
// import { useServerStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// const serverStore = useServerStore()
|
||||
</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="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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.modules.memory-short-term.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,10 +5,18 @@ import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-lucide:brain')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,8 +58,26 @@ const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:brain />
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[70dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0 }"
|
||||
:enter="{ scale: 1, opacity: 1 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:brain />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -59,4 +85,5 @@ const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useShortcutsStore } from '../../../stores/shortcuts'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const shortcutsStore = useShortcutsStore()
|
||||
const { shortcuts } = storeToRefs(shortcutsStore)
|
||||
|
||||
const recordingFor = ref<string | null>(null)
|
||||
const recordingKeys = ref<{
|
||||
modifier: string[]
|
||||
key: string
|
||||
}>({
|
||||
modifier: [],
|
||||
key: '',
|
||||
})
|
||||
|
||||
function startRecording(shortcutType: string) {
|
||||
recordingFor.value = shortcutType
|
||||
}
|
||||
|
||||
function isModifierKey(key: string) {
|
||||
return ['Shift', 'Control', 'Alt', 'Meta'].includes(key)
|
||||
}
|
||||
|
||||
// Handle key combinations
|
||||
useEventListener('keydown', (e) => {
|
||||
if (!recordingFor.value)
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
recordingFor.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (isModifierKey(e.key)) {
|
||||
if (recordingKeys.value.modifier.includes(e.key))
|
||||
return
|
||||
|
||||
recordingKeys.value.modifier.push(e.key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return
|
||||
|
||||
recordingKeys.value.key = e.key.toUpperCase()
|
||||
|
||||
const shortcut = shortcuts.value.find(s => s.type === recordingFor.value)
|
||||
if (shortcut)
|
||||
shortcut.shortcut = `${recordingKeys.value.modifier.join('+')}+${recordingKeys.value.key}`
|
||||
|
||||
recordingKeys.value = {
|
||||
modifier: [],
|
||||
key: '',
|
||||
}
|
||||
recordingFor.value = null
|
||||
}, { passive: false })
|
||||
|
||||
const pressKeysMessage = computed(() => {
|
||||
const i18nMessage = t('settings.pages.shortcuts.sections.section.window-controls.press-keys-hint.label')
|
||||
|
||||
if (recordingKeys.value.modifier.length === 0)
|
||||
return i18nMessage
|
||||
|
||||
return `${i18nMessage}: ${recordingKeys.value.modifier.join(' + ')} + ${recordingKeys.value.key}`
|
||||
})
|
||||
|
||||
function isConflict(shortcut: typeof shortcuts.value[0]) {
|
||||
return shortcuts.value.some(s => s.type !== shortcut.type && s.shortcut === shortcut.shortcut)
|
||||
}
|
||||
|
||||
function toReadable(shortcut: string) {
|
||||
return shortcut.replaceAll('Meta', '⌘').replaceAll('Control', 'Ctrl').replaceAll('Alt', '⌥').replaceAll('Shift', '⇧').replaceAll('+', ' + ')
|
||||
}
|
||||
</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="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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.shortcuts.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div flex-col>
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:window-frame-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.shortcuts.sections.section.window-controls.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4 flex="~ col gap-4">
|
||||
<div
|
||||
v-for="shortcut in shortcuts"
|
||||
:key="shortcut.name"
|
||||
flex="~ row"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250" cursor-pointer
|
||||
items-center justify-between gap-4 rounded-lg px-4 py-3
|
||||
>
|
||||
<div text-base font-medium>
|
||||
{{ t(shortcut.name) }}
|
||||
</div>
|
||||
<div class="shortcut-item" flex="~ row" gap-2 text-sm text="neutral-500 dark:neutral-400">
|
||||
<div v-if="recordingFor === shortcut.type" class="animate-flash animate-duration-2s animate-count-infinite">
|
||||
{{ toReadable(pressKeysMessage) }}
|
||||
</div>
|
||||
<div v-else flex items-center gap-2 @click="startRecording(shortcut.type)">
|
||||
<div>{{ toReadable(shortcut.shortcut) }}</div>
|
||||
<div v-if="isConflict(shortcut)" i-solar:danger-circle-bold-duotone text-red-500 />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:keyboard />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,423 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Collapsable } from '@proj-airi/stage-ui/components'
|
||||
import { DEFAULT_THEME_COLORS_HUE, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { converter } from 'culori'
|
||||
import { TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from 'radix-vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
|
||||
interface ColorPreset {
|
||||
name: string
|
||||
description: string
|
||||
colors: Array<{
|
||||
hex: string
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
|
||||
const colorPresets = computed<ColorPreset[]>(() => [
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.morandi.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.morandi.description'),
|
||||
colors: [
|
||||
{ hex: '#A5978B', name: 'Taupe' },
|
||||
{ hex: '#D8CAAF', name: 'Beige' },
|
||||
{ hex: '#B8B4A7', name: 'Ash Grey' },
|
||||
{ hex: '#C4BCB1', name: 'Light Taupe' },
|
||||
{ hex: '#E5DED8', name: 'Ivory' },
|
||||
{ hex: '#9A8F7D', name: 'Olive Grey' },
|
||||
{ hex: '#BEB5A7', name: 'Sand' },
|
||||
{ hex: '#C9C0B6', name: 'Warm Grey' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.monet.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.monet.description'),
|
||||
colors: [
|
||||
{ hex: '#7A9EAF', name: 'Sky Blue' },
|
||||
{ hex: '#B8C7CC', name: 'Mist' },
|
||||
{ hex: '#D4B79C', name: 'Sand' },
|
||||
{ hex: '#8B9D77', name: 'Moss Green' },
|
||||
{ hex: '#C7D5CB', name: 'Water Lily' },
|
||||
{ hex: '#E6D0B1', name: 'Wheat' },
|
||||
{ hex: '#94A7B1', name: 'Slate Blue' },
|
||||
{ hex: '#B4C8C3', name: 'Sage' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.japanese.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.japanese.description'),
|
||||
colors: [
|
||||
{ hex: '#D9B48F', name: 'Tan' },
|
||||
{ hex: '#B5917A', name: 'Warm Taupe' },
|
||||
{ hex: '#8C7A6B', name: 'Umber' },
|
||||
{ hex: '#A17F5F', name: 'Coffee' },
|
||||
{ hex: '#B98C46', name: 'Bronze' },
|
||||
{ hex: '#C7A252', name: 'Gold' },
|
||||
{ hex: '#DAB300', name: 'Mustard' },
|
||||
{ hex: '#D19826', name: 'Amber' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.nordic.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.nordic.description'),
|
||||
colors: [
|
||||
{ hex: '#9BA7B0', name: 'Nordic Blue' },
|
||||
{ hex: '#C1CBD4', name: 'Ice' },
|
||||
{ hex: '#A5ADB6', name: 'Fjord' },
|
||||
{ hex: '#8B959E', name: 'Steel' },
|
||||
{ hex: '#D4DCE4', name: 'Glacier' },
|
||||
{ hex: '#7F8A94', name: 'Slate' },
|
||||
{ hex: '#B3BCC6', name: 'Cloud' },
|
||||
{ hex: '#98A4AE', name: 'Stone' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t('settings.pages.themes.sections.section.theme-presets.preset.chinese.title'),
|
||||
description: t('settings.pages.themes.sections.section.theme-presets.preset.chinese.description'),
|
||||
colors: [
|
||||
{ hex: '#E4C6D0', name: '霞光红 (Rosy Dawn)' },
|
||||
{ hex: '#A61B29', name: '枣红 (Chinese Red)' },
|
||||
{ hex: '#5D513C', name: '黄栌 (Smoky Brown)' },
|
||||
{ hex: '#789262', name: '竹青 (Bamboo Green)' },
|
||||
{ hex: '#1C0D1A', name: '乌梅紫 (Dark Purple)' },
|
||||
{ hex: '#F7C242', name: '缃色 (Golden Yellow)' },
|
||||
{ hex: '#62A9DD', name: '青冥 (Azure Blue)' },
|
||||
{ hex: '#8C4B3C', name: '赭石 (Ochre)' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
function resetToDefault() {
|
||||
settings.themeColorsHue = DEFAULT_THEME_COLORS_HUE
|
||||
settings.themeColorsHueDynamic = false
|
||||
}
|
||||
|
||||
function applyPrimaryColorFromHex(color: string) {
|
||||
// Convert hex color to OKLCH using culori's converter
|
||||
const oklch = converter('oklch')(color)
|
||||
|
||||
if (!oklch)
|
||||
return
|
||||
|
||||
// Extract hue from OKLCH color
|
||||
const { h } = oklch
|
||||
|
||||
if (!h)
|
||||
return
|
||||
|
||||
// Update theme settings
|
||||
settings.themeColorsHue = h
|
||||
settings.themeColorsHueDynamic = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a color is currently selected based on its hue value
|
||||
* @param hexColor Hex color code to check
|
||||
* @returns True if the color's hue matches the current theme hue
|
||||
*/
|
||||
function isColorSelected(hexColor: string): boolean {
|
||||
// If dynamic coloring is enabled, no preset color is manually selected
|
||||
if (settings.themeColorsHueDynamic)
|
||||
return false
|
||||
|
||||
// Convert hex color to OKLCH
|
||||
const oklch = converter('oklch')(hexColor)
|
||||
if (!oklch || !oklch.h)
|
||||
return false
|
||||
|
||||
// Compare hue values with a small tolerance for floating point comparison
|
||||
const hueDifference = Math.abs(oklch.h - settings.themeColorsHue)
|
||||
return hueDifference < 0.01 || hueDifference > 359.99
|
||||
}
|
||||
</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="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" text-nowrap>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
<div text-nowrap text-3xl font-semibold>
|
||||
{{ t('settings.pages.themes.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div flex-col>
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:pallete-2-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div p-4>
|
||||
<div class="mb-2 text-sm font-medium">
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-4">
|
||||
<input
|
||||
v-model="settings.themeColorsHue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="0.01"
|
||||
class="theme-hue-slider h-10 w-full"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
:class="{ 'opacity-25 cursor-not-allowed': settings.themeColorsHueDynamic }"
|
||||
>
|
||||
|
||||
<div h-10 w-full flex overflow-hidden rounded-lg>
|
||||
<div bg="primary-50" class="primary-color-bar" text-black>
|
||||
50
|
||||
</div>
|
||||
<div bg="primary-100" class="primary-color-bar" text-black>
|
||||
100
|
||||
</div>
|
||||
<div bg="primary-200" class="primary-color-bar" text-black>
|
||||
200
|
||||
</div>
|
||||
<div bg="primary-300" class="primary-color-bar" text-black>
|
||||
300
|
||||
</div>
|
||||
<div bg="primary-400" class="primary-color-bar" text-black>
|
||||
400
|
||||
</div>
|
||||
<div bg="primary-500" class="primary-color-bar" text-black>
|
||||
500
|
||||
</div>
|
||||
<div bg="primary-600" class="primary-color-bar" text-white>
|
||||
600
|
||||
</div>
|
||||
<div bg="primary-700" class="primary-color-bar" text-white>
|
||||
700
|
||||
</div>
|
||||
<div bg="primary-800" class="primary-color-bar" text-white>
|
||||
800
|
||||
</div>
|
||||
<div bg="primary-900" class="primary-color-bar" text-white>
|
||||
900
|
||||
</div>
|
||||
<div bg="primary-950" class="primary-color-bar" text-white>
|
||||
950
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div h-10 w-full flex overflow-hidden rounded-lg class="transparency-grid">
|
||||
<div bg="primary-500/5" class="primary-color-bar" text-black>
|
||||
500/5
|
||||
</div>
|
||||
<div bg="primary-500/10" class="primary-color-bar" text-black>
|
||||
500/10
|
||||
</div>
|
||||
<div bg="primary-500/20" class="primary-color-bar" text-black>
|
||||
500/20
|
||||
</div>
|
||||
<div bg="primary-500/30" class="primary-color-bar" text-black>
|
||||
500/30
|
||||
</div>
|
||||
<div bg="primary-500/40" class="primary-color-bar" text-black>
|
||||
500/40
|
||||
</div>
|
||||
<div bg="primary-500/50" class="primary-color-bar" text-black>
|
||||
500/50
|
||||
</div>
|
||||
<div bg="primary-500/60" class="primary-color-bar" text-black>
|
||||
500/60
|
||||
</div>
|
||||
<div bg="primary-500/70" class="primary-color-bar" text-black>
|
||||
500/70
|
||||
</div>
|
||||
<div bg="primary-500/80" class="primary-color-bar" text-black>
|
||||
500/80
|
||||
</div>
|
||||
<div bg="primary-500/90" class="primary-color-bar" text-black>
|
||||
500/90
|
||||
</div>
|
||||
<div bg="primary-500" class="primary-color-bar" text-black>
|
||||
500
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div mt-4 class="flex items-center justify-end gap-4">
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="settings.themeColorsHueDynamic"
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
>
|
||||
<div
|
||||
class="peer-checked:bg-primary-500 h-6 w-11 rounded-full bg-neutral-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:bg-white dark:bg-neutral-600 after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white"
|
||||
/>
|
||||
<span class="ml-2 text-sm font-medium">{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.rgb-on.title') }}</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="rounded-md bg-neutral-100 px-3 py-1.5 text-sm transition-colors dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
{{ t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.reset.label') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
|
||||
<Collapsable mt-4 w-full :default="true">
|
||||
<template #trigger="slotProps">
|
||||
<button
|
||||
bg="zinc-100 dark:zinc-800"
|
||||
hover="bg-zinc-200 dark:bg-zinc-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
|
||||
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
|
||||
@click="slotProps.setVisible(!slotProps.visible)"
|
||||
>
|
||||
<div flex="~ row 1" items-center gap-1.5>
|
||||
<div
|
||||
i-solar:magic-stick-2-bold-duotone class="provider-icon size-6"
|
||||
transition="filter duration-250 ease-in-out"
|
||||
/>
|
||||
<div>
|
||||
{{ t('settings.pages.themes.sections.section.theme-presets.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
|
||||
<div i-solar:alt-arrow-down-bold-duotone />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div p-4 flex="~ col gap-4">
|
||||
<div
|
||||
v-for="preset in colorPresets"
|
||||
:key="preset.name"
|
||||
flex="~ row"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
transition="all ease-in-out duration-250"
|
||||
cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3
|
||||
>
|
||||
<div>
|
||||
<div text-base font-medium>
|
||||
{{ preset.name }}
|
||||
</div>
|
||||
<div text="sm neutral-500">
|
||||
{{ preset.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div flex="~ row" gap-2>
|
||||
<TooltipProvider v-for="color in preset.colors" :key="color.hex">
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
class="size-6 cursor-pointer rounded-full transition-all duration-250 ease-in-out" :class="[
|
||||
isColorSelected(color.hex)
|
||||
? 'scale-150 z-10 mx-1'
|
||||
: 'hover:scale-110',
|
||||
]"
|
||||
@click="applyPrimaryColorFromHex(color.hex)"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent class="rounded-lg bg-white px-3 py-1.5 text-sm shadow-md dark:bg-neutral-800">
|
||||
{{ color.name }}
|
||||
<TooltipArrow class="fill-white dark:fill-neutral-800" />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsable>
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.primary-color-bar {
|
||||
--at-apply: w-full h-full flex-1 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.theme-hue-slider {
|
||||
--at-apply: rounded-lg appearance-none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
oklch(85% 0.2 0),
|
||||
oklch(85% 0.2 60),
|
||||
oklch(85% 0.2 120),
|
||||
oklch(85% 0.2 180),
|
||||
oklch(85% 0.2 240),
|
||||
oklch(85% 0.2 300),
|
||||
oklch(85% 0.2 360)
|
||||
);
|
||||
}
|
||||
|
||||
.theme-hue-slider::-webkit-slider-thumb {
|
||||
--at-apply: w-2 h-12 appearance-none rounded-md bg-neutral-500/80 dark: bg-neutral-400/80 cursor-pointer shadow-md
|
||||
border-2 border-white hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.theme-hue-slider::-moz-range-thumb {
|
||||
--at-apply: w-2 h-12 bg-neutral-500/80 rounded-md dark: bg-neutral-400/80 cursor-pointer shadow-md border-2
|
||||
border-white border-box hover: bg-neutral-500 dark: hover: bg-neutral-400 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.transparency-grid {
|
||||
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
@@ -320,8 +320,15 @@ function getModuleShortName(id: string, module: 'consciousness' | 'voice') {
|
||||
/>
|
||||
|
||||
<!-- Background decoration -->
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:id-card />
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[72dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:id-card />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -48,19 +48,40 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Section :title="t('settings.sections.section.general.title')" icon="i-solar:filters-bold-duotone">
|
||||
<Section
|
||||
v-motion
|
||||
:title="t('settings.sections.section.general.title')"
|
||||
icon="i-solar:filters-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (1 * 10)"
|
||||
:delay="1 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<!-- Theme Setting -->
|
||||
<CheckBar
|
||||
v-model="dark"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (2 * 10)"
|
||||
:delay="2 * 50"
|
||||
icon-on="i-solar:moon-stars-bold-duotone"
|
||||
icon-off="i-solar:sun-fog-bold-duotone"
|
||||
text="settings.theme"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<!-- Language Setting -->
|
||||
<div
|
||||
v-motion
|
||||
class="w-full flex items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (3 * 10)"
|
||||
:delay="3 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
{{ $t('settings.language.title') }}
|
||||
<select
|
||||
@@ -78,8 +99,25 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section :title="t('settings.pages.themes.sections.section.custom-color.title')" icon="i-solar:pallete-2-bold-duotone">
|
||||
<div flex items-center justify-between>
|
||||
<Section
|
||||
v-motion
|
||||
:title="t('settings.pages.themes.sections.section.custom-color.title')"
|
||||
icon="i-solar:pallete-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (4 * 10)"
|
||||
:delay="4 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div
|
||||
v-motion flex items-center
|
||||
justify-between
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (5 * 10)"
|
||||
:delay="5 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span text-lg font-semibold>{{ $t('settings.pages.themes.sections.section.custom-color.fields.field.primary-color.label') }}</span>
|
||||
<label relative flex cursor-pointer items-center gap-2>
|
||||
<input
|
||||
@@ -95,18 +133,40 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
</div>
|
||||
<input
|
||||
v-model="settings.themeColorsHue"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (6 * 10)"
|
||||
:delay="6 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
type="range" min="0" max="360" step="0.01" class="theme-hue-slider"
|
||||
:disabled="settings.themeColorsHueDynamic"
|
||||
:class="settings.themeColorsHueDynamic ? 'opacity-25 cursor-not-allowed' : 'cursor-pointer'"
|
||||
>
|
||||
<div class="color-bar">
|
||||
<div
|
||||
v-motion
|
||||
class="color-bar"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (7 * 10)"
|
||||
:delay="7 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg-primary-50>50</span>
|
||||
<span bg-primary-100>100</span>
|
||||
<span bg-primary-200>200</span>
|
||||
<span bg-primary-300>300</span>
|
||||
<span bg-primary-400>400</span>
|
||||
<span bg-primary-500>500</span>
|
||||
<div text-white>
|
||||
<div
|
||||
v-motion
|
||||
text-white
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (8 * 10)"
|
||||
:delay="8 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg-primary-600>600</span>
|
||||
<span bg-primary-700>700</span>
|
||||
<span bg-primary-800>800</span>
|
||||
@@ -114,7 +174,15 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
<span bg-primary-950>950</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-bar transparency-grid">
|
||||
<div
|
||||
v-motion
|
||||
class="color-bar transparency-grid"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (9 * 10)"
|
||||
:delay="9 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<span bg="primary-500/5">500/5</span>
|
||||
<span bg="primary-500/10">500/10</span>
|
||||
<span bg="primary-500/20">500/20</span>
|
||||
@@ -129,12 +197,26 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="settings.pages.themes.sections.section.theme-presets.title" icon="i-solar:magic-stick-2-bold-duotone">
|
||||
<Section
|
||||
v-motion title="settings.pages.themes.sections.section.theme-presets.title"
|
||||
icon="i-solar:magic-stick-2-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (10 * 10)"
|
||||
:delay="10 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div
|
||||
v-for="({ title, description, colors }, i) in $tm('settings.pages.themes.sections.section.theme-presets.presets')" :key="i"
|
||||
v-motion
|
||||
class="w-full flex items-center justify-between rounded-lg px-4 py-3 outline-none transition-all duration-250 ease-in-out"
|
||||
bg="neutral-100 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (11 * 10) + (i * 10)"
|
||||
:delay="11 * 50 + (i * 50)"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<div>
|
||||
<span font-medium>{{ $rt(title) }}</span>
|
||||
@@ -146,20 +228,40 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="settings.pages.themes.sections.section.developer.title" icon="i-solar:code-bold-duotone">
|
||||
<Section
|
||||
v-motion title="settings.pages.themes.sections.section.developer.title"
|
||||
icon="i-solar:code-bold-duotone"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (18 * 10)"
|
||||
:delay="18 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
>
|
||||
<CheckBar
|
||||
v-model="settings.disableTransitions"
|
||||
v-motion
|
||||
icon-on="i-solar:people-nearby-bold-duotone"
|
||||
icon-off="i-solar:running-2-line-duotone"
|
||||
text="settings.animations.stage-transitions.title"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (19 * 10)"
|
||||
:delay="19 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="settings.usePageSpecificTransitions"
|
||||
v-motion
|
||||
:disabled="settings.disableTransitions"
|
||||
icon-on="i-solar:running-2-line-duotone"
|
||||
icon-off="i-solar:people-nearby-bold-duotone"
|
||||
text="settings.animations.use-page-specific-transitions.title"
|
||||
description="settings.animations.use-page-specific-transitions.description"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (20 * 10)"
|
||||
:delay="20 * 50"
|
||||
transition="all ease-in-out duration-250"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -174,8 +276,15 @@ watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions],
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div v-if="!settings.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:paintbrush />
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 30 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -130,10 +130,17 @@ const settings = computed(() => [
|
||||
:description="setting.description"
|
||||
:icon="setting.icon"
|
||||
:to="setting.to"
|
||||
@click="(e) => handleIconItemClick(e, setting)"
|
||||
@click="(e: Event) => handleIconItemClick(e, setting)"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[75dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, rotate: 45 }"
|
||||
:enter="{ scale: 1, opacity: 1, rotate: 0 }"
|
||||
:duration="500"
|
||||
>
|
||||
<div v-motion text="60" i-lucide:cog />
|
||||
</div>
|
||||
<IconAnimation
|
||||
|
||||
@@ -35,5 +35,15 @@ const { t } = useI18n()
|
||||
{{ t('settings.pages.memory.title') }}
|
||||
</div>
|
||||
</h1>
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[65dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 40 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:sprout />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -10,7 +9,6 @@ import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const settingsStore = useSettings()
|
||||
|
||||
interface Module {
|
||||
id: string
|
||||
@@ -164,8 +162,15 @@ const {
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
/>
|
||||
<div v-if="!settingsStore.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:blocks />
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[72dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0, y: 0 }"
|
||||
:enter="{ scale: 1, opacity: 1, y: 10 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:blocks />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -11,7 +11,6 @@ const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
@@ -70,8 +69,15 @@ const {
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div v-if="!settingsStore.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:brain />
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[70dvh]" right--15 z--1
|
||||
:initial="{ scale: 0.9, opacity: 0 }"
|
||||
:enter="{ scale: 1, opacity: 1 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" i-lucide:brain />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const props = defineProps<{
|
||||
<template>
|
||||
<div
|
||||
flex="~ col"
|
||||
bg="neutral-200/50 dark:neutral-800/50"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
border="neutral-100 dark:neutral-800/25 hover:primary-500/30 dark:hover:primary-400/30 solid 2"
|
||||
drop-shadow="none hover:[0px_4px_4px_rgba(220,220,220,0.4)] active:[0px_0px_0px_rgba(220,220,220,0.25)] dark:hover:none"
|
||||
class="menu-icon-status-item"
|
||||
|
||||
@@ -29,7 +29,14 @@ defineProps<{
|
||||
</h1>
|
||||
</div>
|
||||
<slot />
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" :class="providerIcon || providerIconColor" />
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
fixed top="[70dvh]" right-0 z--1
|
||||
:initial="{ scale: 0.95, opacity: 0, x: 20 }"
|
||||
:enter="{ scale: 1, opacity: 1, x: 0 }"
|
||||
:duration="250"
|
||||
>
|
||||
<div text="60" :class="providerIcon || providerIconColor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -175,7 +175,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'vLLM',
|
||||
descriptionKey: 'settings.pages.providers.provider.vllm.description',
|
||||
description: 'vllm.ai',
|
||||
iconColor: 'i-lobe-icons:vllm-color',
|
||||
iconColor: 'i-lobe-icons:vllm',
|
||||
createProvider: config => createOllama((config.baseUrl as string).trim()),
|
||||
capabilities: {
|
||||
listModels: async () => {
|
||||
@@ -379,7 +379,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'DeepSeek',
|
||||
descriptionKey: 'settings.pages.providers.provider.deepseek.description',
|
||||
description: 'deepseek.com',
|
||||
iconColor: 'i-lobe-icons:deepseek-color',
|
||||
iconColor: 'i-lobe-icons:deepseek',
|
||||
createProvider: config => createDeepSeek((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
|
||||
capabilities: {
|
||||
listModels: async (config) => {
|
||||
@@ -468,7 +468,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'Microsoft / Azure Speech',
|
||||
descriptionKey: 'settings.pages.providers.provider.microsoft-speech.description',
|
||||
description: 'speech.microsoft.com',
|
||||
iconColor: 'i-lobe-icons:microsoft-color',
|
||||
iconColor: 'i-lobe-icons:microsoft',
|
||||
defaultOptions: {
|
||||
baseUrl: 'https://unspeech.hyp3r.link/v1/',
|
||||
},
|
||||
@@ -512,7 +512,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'Together.ai',
|
||||
descriptionKey: 'settings.pages.providers.provider.together.description',
|
||||
description: 'together.ai',
|
||||
iconColor: 'i-lobe-icons:together-color',
|
||||
iconColor: 'i-lobe-icons:together',
|
||||
createProvider: config => createTogetherAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
|
||||
capabilities: {
|
||||
listModels: async (config) => {
|
||||
@@ -537,7 +537,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'Novita',
|
||||
descriptionKey: 'settings.pages.providers.provider.novita.description',
|
||||
description: 'novita.ai',
|
||||
iconColor: 'i-lobe-icons:novita-color',
|
||||
iconColor: 'i-lobe-icons:novita',
|
||||
createProvider: config => createNovita((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
|
||||
capabilities: {
|
||||
listModels: async (config) => {
|
||||
@@ -615,7 +615,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'Cloudflare Workers AI',
|
||||
descriptionKey: 'settings.pages.providers.provider.cloudflare-workers-ai.description',
|
||||
description: 'cloudflare.com',
|
||||
iconColor: 'i-lobe-icons:cloudflare-color',
|
||||
iconColor: 'i-lobe-icons:cloudflare',
|
||||
createProvider: config => createWorkersAI((config.apiKey as string).trim(), config.accountId as string),
|
||||
capabilities: {
|
||||
listModels: async () => {
|
||||
@@ -682,7 +682,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
name: 'Mistral',
|
||||
descriptionKey: 'settings.pages.providers.provider.mistral.description',
|
||||
description: 'mistral.ai',
|
||||
iconColor: 'i-lobe-icons:mistral-color',
|
||||
iconColor: 'i-lobe-icons:mistral',
|
||||
createProvider: config => createMistral((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
|
||||
capabilities: {
|
||||
listModels: async (config) => {
|
||||
|
||||
Reference in New Issue
Block a user