Skip to content

Commit 84dbf3f

Browse files
authored
feat(stage-ui): improve animation delay in provider settings (#743)
1 parent c13ada5 commit 84dbf3f

File tree

2 files changed

+159
-87
lines changed

2 files changed

+159
-87
lines changed

packages/stage-pages/src/pages/settings/providers/index.vue

Lines changed: 141 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,130 @@
11
<script setup lang="ts">
2+
import type { ProviderMetadata } from '@proj-airi/stage-ui/stores/providers'
3+
import type { Ref } from 'vue'
4+
25
import { IconStatusItem } from '@proj-airi/stage-ui/components'
36
import { useScrollToHash } from '@proj-airi/stage-ui/composables/useScrollToHash'
47
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
8+
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
59
import { storeToRefs } from 'pinia'
6-
import { useRoute } from 'vue-router'
10+
import { computed } from 'vue'
11+
import { onBeforeRouteLeave, useRoute } from 'vue-router'
12+
13+
import { useProvidersPageStore } from './store'
14+
15+
interface ProviderBlock {
16+
id: string
17+
icon: string
18+
title: string
19+
description: string
20+
providersRef: Ref<ProviderMetadata[]>
21+
}
22+
23+
interface ProviderRenderData extends ProviderMetadata {
24+
renderIndex: number
25+
}
26+
27+
interface ComputedProviderBlock {
28+
id: string
29+
icon: string
30+
title: string
31+
description: string
32+
providers: ProviderRenderData[]
33+
}
734
835
const route = useRoute()
936
const providersStore = useProvidersStore()
37+
const providersPageStore = useProvidersPageStore()
38+
const breakpoints = useBreakpoints(breakpointsTailwind)
39+
1040
const {
1141
allChatProvidersMetadata,
1242
allAudioSpeechProvidersMetadata,
1343
allAudioTranscriptionProvidersMetadata,
1444
} = storeToRefs(providersStore)
1545
46+
const { lastClickedProviderIndex } = storeToRefs(providersPageStore)
47+
48+
const providerBlocksConfig: ProviderBlock[] = [
49+
{
50+
id: 'chat',
51+
icon: 'i-solar:chat-square-like-bold-duotone',
52+
title: 'Chat',
53+
description: 'Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.',
54+
providersRef: allChatProvidersMetadata,
55+
},
56+
{
57+
id: 'speech',
58+
icon: 'i-solar:user-speak-rounded-bold-duotone',
59+
title: 'Speech',
60+
description: 'Speech (text-to-speech) model providers. e.g. ElevenLabs, Azure Speech.',
61+
providersRef: allAudioSpeechProvidersMetadata,
62+
},
63+
{
64+
id: 'transcription',
65+
icon: 'i-solar:microphone-3-bold-duotone',
66+
title: 'Transcription',
67+
description: 'Transcription (speech-to-text) model providers. e.g. Whisper.cpp, OpenAI, Azure Speech',
68+
providersRef: allAudioTranscriptionProvidersMetadata,
69+
},
70+
]
71+
72+
const providerBlocks = computed<ComputedProviderBlock[]>(() => {
73+
let globalIndex = 0
74+
return providerBlocksConfig.map(block => ({
75+
id: block.id,
76+
icon: block.icon,
77+
title: block.title,
78+
description: block.description,
79+
providers: block.providersRef.value.map(provider => ({
80+
...provider,
81+
renderIndex: globalIndex++,
82+
})),
83+
}))
84+
})
85+
86+
const cols = computed(() => {
87+
if (breakpoints.greaterOrEqual('xl').value) {
88+
return 3
89+
}
90+
if (breakpoints.greaterOrEqual('sm').value) {
91+
return 2
92+
}
93+
return 1
94+
})
95+
96+
function getAnimationDelay(renderIndex: number) {
97+
const numCols = cols.value
98+
99+
const currentRow = Math.floor(renderIndex / numCols)
100+
const currentCol = renderIndex % numCols
101+
102+
const clickedRow = Math.floor(lastClickedProviderIndex.value / numCols)
103+
const clickedCol = lastClickedProviderIndex.value % numCols
104+
105+
// manhattan distance
106+
const distance = Math.abs(currentRow - clickedRow) + Math.abs(currentCol - clickedCol)
107+
108+
return distance * 80
109+
}
110+
111+
function handleProviderClick(renderIndex: number) {
112+
providersPageStore.setLastClickedProviderIndex(renderIndex)
113+
}
114+
16115
useScrollToHash(() => route.hash, {
17116
auto: true, // automatically react to route hash
18117
offset: 16, // header + margin spacing
19118
behavior: 'smooth', // smooth scroll animation
20-
maxRetries: 15, // retry if target element isnt ready
119+
maxRetries: 15, // retry if target element isn't ready
21120
retryDelay: 150, // wait between retries
22121
})
122+
123+
onBeforeRouteLeave((to, from) => {
124+
if (!to.path.startsWith('/settings/providers/')) {
125+
providersPageStore.resetLastClickedProviderIndex()
126+
}
127+
})
23128
</script>
24129

25130
<template>
@@ -39,99 +144,48 @@ useScrollToHash(() => route.hash, {
39144
</i18n-t>
40145
</div>
41146
</div>
42-
<div flex="~ row items-center gap-2">
43-
<div id="chat" i-solar:chat-square-like-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
44-
<div>
147+
148+
<template v-for="(block, blockIndex) in providerBlocks" :key="block.id">
149+
<div flex="~ row items-center gap-2" :class="{ 'my-5': blockIndex > 0 }">
150+
<div :id="block.id" :class="block.icon" text="neutral-500 dark:neutral-400 4xl" />
45151
<div>
46-
<span text="neutral-300 dark:neutral-500 sm sm:base">Text generation model providers. e.g. OpenRouter, OpenAI, Ollama.</span>
47-
</div>
48-
<div flex text-nowrap text="2xl sm:3xl" font-normal>
49152
<div>
50-
Chat
153+
<span text="neutral-300 dark:neutral-500 sm sm:base">{{ block.description }}</span>
51154
</div>
52-
</div>
53-
</div>
54-
</div>
55-
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
56-
<IconStatusItem
57-
v-for="(provider, index) of allChatProvidersMetadata"
58-
:key="provider.id"
59-
v-motion
60-
:initial="{ opacity: 0, y: 10 }"
61-
:enter="{ opacity: 1, y: 0 }"
62-
:duration="250 + index * 10"
63-
:delay="index * 50"
64-
:title="provider.localizedName || 'Unknown'"
65-
:description="provider.localizedDescription"
66-
:icon="provider.icon"
67-
:icon-color="provider.iconColor"
68-
:icon-image="provider.iconImage"
69-
:to="`/settings/providers/${provider.category}/${provider.id}`"
70-
:configured="provider.configured"
71-
/>
72-
</div>
73-
<div flex="~ row items-center gap-2" my-5>
74-
<div id="speech" i-solar:user-speak-rounded-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
75-
<div>
76-
<div>
77-
<span text="neutral-300 dark:neutral-500 sm sm:base">Speech (text-to-speech) model providers. e.g. ElevenLabs, Azure Speech.</span>
78-
</div>
79-
<div flex text-nowrap text="2xl sm:3xl" font-normal>
80-
<div>
81-
Speech
155+
<div flex text-nowrap text="2xl sm:3xl" font-normal>
156+
<div>
157+
{{ block.title }}
158+
</div>
82159
</div>
83160
</div>
84161
</div>
85-
</div>
86-
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
87-
<IconStatusItem
88-
v-for="(provider, index) of allAudioSpeechProvidersMetadata"
89-
:key="provider.id"
90-
v-motion
91-
:initial="{ opacity: 0, y: 10 }"
92-
:enter="{ opacity: 1, y: 0 }"
93-
:duration="250 + index * 10"
94-
:delay="(allChatProvidersMetadata.length + index) * 50"
95-
:title="provider.localizedName || 'Unknown'"
96-
:description="provider.localizedDescription"
97-
:icon="provider.icon"
98-
:icon-color="provider.iconColor"
99-
:icon-image="provider.iconImage"
100-
:to="`/settings/providers/${provider.category}/${provider.id}`"
101-
:configured="provider.configured"
102-
/>
103-
</div>
104-
<div flex="~ row items-center gap-2" my-5>
105-
<div id="transcription" i-solar:microphone-3-bold-duotone text="neutral-500 dark:neutral-400 4xl" />
106-
<div>
107-
<div>
108-
<span text="neutral-300 dark:neutral-500 sm sm:base">Transcription (speech-to-text) model providers. e.g. Whisper.cpp, OpenAI, Azure Speech</span>
109-
</div>
110-
<div flex text-nowrap text="2xl sm:3xl" font-normal>
111-
<div>
112-
Transcription
113-
</div>
162+
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
163+
<div
164+
v-for="provider of block.providers"
165+
:key="provider.id"
166+
v-motion
167+
:initial="{ opacity: 0, y: 10 }"
168+
:enter="{ opacity: 1,
169+
y: 0,
170+
transition: {
171+
duration: 250,
172+
delay: getAnimationDelay(provider.renderIndex),
173+
} }"
174+
:to="`/settings/providers/${provider.category}/${provider.id}`"
175+
@click="handleProviderClick(provider.renderIndex)"
176+
>
177+
<IconStatusItem
178+
:title="provider.localizedName || 'Unknown'"
179+
:description="provider.localizedDescription"
180+
:icon="provider.icon"
181+
:icon-color="provider.iconColor"
182+
:icon-image="provider.iconImage"
183+
:to="`/settings/providers/${provider.category}/${provider.id}`"
184+
:configured="provider.configured"
185+
/>
114186
</div>
115187
</div>
116-
</div>
117-
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
118-
<IconStatusItem
119-
v-for="(provider, index) of allAudioTranscriptionProvidersMetadata"
120-
:key="provider.id"
121-
v-motion
122-
:initial="{ opacity: 0, y: 10 }"
123-
:enter="{ opacity: 1, y: 0 }"
124-
:duration="250 + index * 10"
125-
:delay="(allChatProvidersMetadata.length + allAudioSpeechProvidersMetadata.length + index) * 50"
126-
:title="provider.localizedName || 'Unknown'"
127-
:description="provider.localizedDescription"
128-
:icon="provider.icon"
129-
:icon-color="provider.iconColor"
130-
:icon-image="provider.iconImage"
131-
:to="`/settings/providers/${provider.category}/${provider.id}`"
132-
:configured="provider.configured"
133-
/>
134-
</div>
188+
</template>
135189
</div>
136190
<div
137191
v-motion
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineStore } from 'pinia'
2+
3+
export const useProvidersPageStore = defineStore('providersPage', {
4+
state: () => ({
5+
lastClickedProviderIndex: 0,
6+
}),
7+
8+
actions: {
9+
setLastClickedProviderIndex(index: number) {
10+
this.lastClickedProviderIndex = index
11+
},
12+
13+
resetLastClickedProviderIndex() {
14+
this.lastClickedProviderIndex = 0
15+
},
16+
},
17+
})
18+

0 commit comments

Comments
 (0)