feat(stage-ui): improve animation delay in provider settings (#743)
This commit is contained in:
@@ -1,25 +1,130 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ProviderMetadata } from '@proj-airi/stage-ui/stores/providers'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||||
import { useScrollToHash } from '@proj-airi/stage-ui/composables/useScrollToHash'
|
import { useScrollToHash } from '@proj-airi/stage-ui/composables/useScrollToHash'
|
||||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useRoute } from 'vue-router'
|
import { computed } from 'vue'
|
||||||
|
import { onBeforeRouteLeave, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { useProvidersPageStore } from './store'
|
||||||
|
|
||||||
|
interface ProviderBlock {
|
||||||
|
id: string
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
providersRef: Ref<ProviderMetadata[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderRenderData extends ProviderMetadata {
|
||||||
|
renderIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComputedProviderBlock {
|
||||||
|
id: string
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
providers: ProviderRenderData[]
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const providersStore = useProvidersStore()
|
const providersStore = useProvidersStore()
|
||||||
|
const providersPageStore = useProvidersPageStore()
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
allChatProvidersMetadata,
|
allChatProvidersMetadata,
|
||||||
allAudioSpeechProvidersMetadata,
|
allAudioSpeechProvidersMetadata,
|
||||||
allAudioTranscriptionProvidersMetadata,
|
allAudioTranscriptionProvidersMetadata,
|
||||||
} = storeToRefs(providersStore)
|
} = storeToRefs(providersStore)
|
||||||
|
|
||||||
|
const { lastClickedProviderIndex } = storeToRefs(providersPageStore)
|
||||||
|
|
||||||
|
const providerBlocksConfig: ProviderBlock[] = [
|
||||||
|
{
|
||||||
|
id: 'chat',
|
||||||
|
icon: 'i-solar:chat-square-like-bold-duotone',
|
||||||
|
title: 'Chat',
|
||||||
|
description: 'Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.',
|
||||||
|
providersRef: allChatProvidersMetadata,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'speech',
|
||||||
|
icon: 'i-solar:user-speak-rounded-bold-duotone',
|
||||||
|
title: 'Speech',
|
||||||
|
description: 'Speech (text-to-speech) model providers. e.g. ElevenLabs, Azure Speech.',
|
||||||
|
providersRef: allAudioSpeechProvidersMetadata,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transcription',
|
||||||
|
icon: 'i-solar:microphone-3-bold-duotone',
|
||||||
|
title: 'Transcription',
|
||||||
|
description: 'Transcription (speech-to-text) model providers. e.g. Whisper.cpp, OpenAI, Azure Speech',
|
||||||
|
providersRef: allAudioTranscriptionProvidersMetadata,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const providerBlocks = computed<ComputedProviderBlock[]>(() => {
|
||||||
|
let globalIndex = 0
|
||||||
|
return providerBlocksConfig.map(block => ({
|
||||||
|
id: block.id,
|
||||||
|
icon: block.icon,
|
||||||
|
title: block.title,
|
||||||
|
description: block.description,
|
||||||
|
providers: block.providersRef.value.map(provider => ({
|
||||||
|
...provider,
|
||||||
|
renderIndex: globalIndex++,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const cols = computed(() => {
|
||||||
|
if (breakpoints.greaterOrEqual('xl').value) {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if (breakpoints.greaterOrEqual('sm').value) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
|
||||||
|
function getAnimationDelay(renderIndex: number) {
|
||||||
|
const numCols = cols.value
|
||||||
|
|
||||||
|
const currentRow = Math.floor(renderIndex / numCols)
|
||||||
|
const currentCol = renderIndex % numCols
|
||||||
|
|
||||||
|
const clickedRow = Math.floor(lastClickedProviderIndex.value / numCols)
|
||||||
|
const clickedCol = lastClickedProviderIndex.value % numCols
|
||||||
|
|
||||||
|
// manhattan distance
|
||||||
|
const distance = Math.abs(currentRow - clickedRow) + Math.abs(currentCol - clickedCol)
|
||||||
|
|
||||||
|
return distance * 80
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProviderClick(renderIndex: number) {
|
||||||
|
providersPageStore.setLastClickedProviderIndex(renderIndex)
|
||||||
|
}
|
||||||
|
|
||||||
useScrollToHash(() => route.hash, {
|
useScrollToHash(() => route.hash, {
|
||||||
auto: true, // automatically react to route hash
|
auto: true, // automatically react to route hash
|
||||||
offset: 16, // header + margin spacing
|
offset: 16, // header + margin spacing
|
||||||
behavior: 'smooth', // smooth scroll animation
|
behavior: 'smooth', // smooth scroll animation
|
||||||
maxRetries: 15, // retry if target element isn’t ready
|
maxRetries: 15, // retry if target element isn't ready
|
||||||
retryDelay: 150, // wait between retries
|
retryDelay: 150, // wait between retries
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeRouteLeave((to, from) => {
|
||||||
|
if (!to.path.startsWith('/settings/providers/')) {
|
||||||
|
providersPageStore.resetLastClickedProviderIndex()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,99 +144,48 @@ useScrollToHash(() => route.hash, {
|
|||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div flex="~ row items-center gap-2">
|
|
||||||
<div id="chat" i-solar:chat-square-like-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
|
<template v-for="(block, blockIndex) in providerBlocks" :key="block.id">
|
||||||
<div>
|
<div flex="~ row items-center gap-2" :class="{ 'my-5': blockIndex > 0 }">
|
||||||
|
<div :id="block.id" :class="block.icon" text="neutral-500 dark:neutral-400 4xl" />
|
||||||
<div>
|
<div>
|
||||||
<span text="neutral-300 dark:neutral-500 sm sm:base">Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.</span>
|
|
||||||
</div>
|
|
||||||
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
|
||||||
<div>
|
<div>
|
||||||
Chat
|
<span text="neutral-300 dark:neutral-500 sm sm:base">{{ block.description }}</span>
|
||||||
|
</div>
|
||||||
|
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
||||||
|
<div>
|
||||||
|
{{ block.title }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
||||||
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
<div
|
||||||
<IconStatusItem
|
v-for="provider of block.providers"
|
||||||
v-for="(provider, index) of allChatProvidersMetadata"
|
:key="provider.id"
|
||||||
:key="provider.id"
|
v-motion
|
||||||
v-motion
|
:initial="{ opacity: 0, y: 10 }"
|
||||||
:initial="{ opacity: 0, y: 10 }"
|
:enter="{ opacity: 1,
|
||||||
:enter="{ opacity: 1, y: 0 }"
|
y: 0,
|
||||||
:duration="250 + index * 10"
|
transition: {
|
||||||
:delay="index * 50"
|
duration: 250,
|
||||||
:title="provider.localizedName || 'Unknown'"
|
delay: getAnimationDelay(provider.renderIndex),
|
||||||
:description="provider.localizedDescription"
|
} }"
|
||||||
:icon="provider.icon"
|
:to="`/settings/providers/${provider.category}/${provider.id}`"
|
||||||
:icon-color="provider.iconColor"
|
@click="handleProviderClick(provider.renderIndex)"
|
||||||
:icon-image="provider.iconImage"
|
>
|
||||||
:to="`/settings/providers/${provider.category}/${provider.id}`"
|
<IconStatusItem
|
||||||
:configured="provider.configured"
|
:title="provider.localizedName || 'Unknown'"
|
||||||
/>
|
:description="provider.localizedDescription"
|
||||||
</div>
|
:icon="provider.icon"
|
||||||
<div flex="~ row items-center gap-2" my-5>
|
:icon-color="provider.iconColor"
|
||||||
<div id="speech" i-solar:user-speak-rounded-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
|
:icon-image="provider.iconImage"
|
||||||
<div>
|
:to="`/settings/providers/${provider.category}/${provider.id}`"
|
||||||
<div>
|
:configured="provider.configured"
|
||||||
<span text="neutral-300 dark:neutral-500 sm sm:base">Speech (text-to-speech) model providers. e.g. ElevenLabs, Azure Speech.</span>
|
/>
|
||||||
</div>
|
|
||||||
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
|
||||||
<div>
|
|
||||||
Speech
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
|
||||||
<IconStatusItem
|
|
||||||
v-for="(provider, index) of allAudioSpeechProvidersMetadata"
|
|
||||||
:key="provider.id"
|
|
||||||
v-motion
|
|
||||||
:initial="{ opacity: 0, y: 10 }"
|
|
||||||
:enter="{ opacity: 1, y: 0 }"
|
|
||||||
:duration="250 + index * 10"
|
|
||||||
:delay="(allChatProvidersMetadata.length + index) * 50"
|
|
||||||
:title="provider.localizedName || 'Unknown'"
|
|
||||||
:description="provider.localizedDescription"
|
|
||||||
:icon="provider.icon"
|
|
||||||
:icon-color="provider.iconColor"
|
|
||||||
:icon-image="provider.iconImage"
|
|
||||||
:to="`/settings/providers/${provider.category}/${provider.id}`"
|
|
||||||
:configured="provider.configured"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div flex="~ row items-center gap-2" my-5>
|
|
||||||
<div id="transcription" i-solar:microphone-3-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<span text="neutral-300 dark:neutral-500 sm sm:base">Transcription (speech-to-text) model providers. e.g. Whisper.cpp, OpenAI, Azure Speech</span>
|
|
||||||
</div>
|
|
||||||
<div flex text-nowrap text="2xl sm:3xl" font-normal>
|
|
||||||
<div>
|
|
||||||
Transcription
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
|
|
||||||
<IconStatusItem
|
|
||||||
v-for="(provider, index) of allAudioTranscriptionProvidersMetadata"
|
|
||||||
:key="provider.id"
|
|
||||||
v-motion
|
|
||||||
:initial="{ opacity: 0, y: 10 }"
|
|
||||||
:enter="{ opacity: 1, y: 0 }"
|
|
||||||
:duration="250 + index * 10"
|
|
||||||
:delay="(allChatProvidersMetadata.length + allAudioSpeechProvidersMetadata.length + index) * 50"
|
|
||||||
:title="provider.localizedName || 'Unknown'"
|
|
||||||
:description="provider.localizedDescription"
|
|
||||||
:icon="provider.icon"
|
|
||||||
:icon-color="provider.iconColor"
|
|
||||||
:icon-image="provider.iconImage"
|
|
||||||
:to="`/settings/providers/${provider.category}/${provider.id}`"
|
|
||||||
:configured="provider.configured"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-motion
|
v-motion
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useProvidersPageStore = defineStore('providersPage', {
|
||||||
|
state: () => ({
|
||||||
|
lastClickedProviderIndex: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
setLastClickedProviderIndex(index: number) {
|
||||||
|
this.lastClickedProviderIndex = index
|
||||||
|
},
|
||||||
|
|
||||||
|
resetLastClickedProviderIndex() {
|
||||||
|
this.lastClickedProviderIndex = 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
Reference in New Issue
Block a user