Skip to content

Commit 7afb1bd

Browse files
committed
switch to CommandPalette
1 parent 5fea339 commit 7afb1bd

File tree

6 files changed

+607
-94
lines changed

6 files changed

+607
-94
lines changed

AiServer/wwwroot/Ui.mjs

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
import { ref, computed, inject, onMounted, shallowRef, watch, nextTick } from "vue"
1+
import { ref, computed, inject, onMounted, onUnmounted, shallowRef, watch, nextTick } from "vue"
22
import { useClient, useAuth } from "@servicestack/vue"
33
import { Authenticate } from "dtos"
44

5+
import { Features, Components, FeatureGroups } from "./mjs/components/Features.mjs";
56
import SignIn from "/mjs/components/SignIn.mjs"
6-
import Chat from "/mjs/components/Chat.mjs"
7-
import TextToImage from "/mjs/components/TextToImage.mjs"
8-
import ImageToText from "/mjs/components/ImageToText.mjs"
9-
import ImageToImage from "/mjs/components/ImageToImage.mjs"
10-
import ImageUpscale from "/mjs/components/ImageUpscale.mjs"
11-
import SpeechToText from "/mjs/components/SpeechToText.mjs"
12-
import TextToSpeech from "/mjs/components/TextToSpeech.mjs"
13-
import Transform from "/mjs/components/Transform.mjs"
147
import UiHome from "/mjs/components/UiHome.mjs"
158
import SignInForm from "/mjs/components/SignInForm.mjs"
169
import ShellCommand from "./mjs/components/ShellCommand.mjs"
10+
import CommandPalette from "./mjs/components/CommandPalette.mjs"
1711
import { prefixes, icons, uiLabel } from "/mjs/utils.mjs"
1812

1913
const HomeSection = {
@@ -22,26 +16,17 @@ const HomeSection = {
2216
component: UiHome
2317
}
2418

25-
const components = {
26-
Chat,
27-
TextToImage,
28-
ImageToText,
29-
ImageToImage,
30-
ImageUpscale,
31-
SpeechToText,
32-
TextToSpeech,
33-
// Transform,
34-
}
35-
3619
export default {
3720
components: {
3821
SignIn,
3922
SignInForm,
4023
ShellCommand,
41-
...components,
24+
CommandPalette,
25+
...Components,
4226
},
4327
template: `
4428
<div class="min-h-full">
29+
<CommandPalette v-if="showPalette" @done="showPalette=false" />
4530
<nav class="border-b border-gray-200 bg-white">
4631
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
4732
<div class="flex h-16 justify-between">
@@ -52,13 +37,26 @@ export default {
5237
</a>
5338
</div>
5439
<div class="hidden sm:-my-px sm:ml-2 lg:ml-4 sm:flex sm:space-x-4 xl:space-x-6">
55-
<a v-href="{admin:section.id,id:undefined}" v-for="section in sections"
56-
:class="['inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium', routes.admin==section.id ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']" aria-current="page">
40+
<a v-for="section in FeatureGroups" v-href="{admin:section.features[0]?.id,id:undefined}" aria-current="page"
41+
:class="['inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium',
42+
section.features.some(x => x.id === routes.admin) ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']">
5743
<span class="lg:hidden xl:inline mr-2" :title="section.label">
5844
<img :src="section.icon" class="w-6 h-6" :alt="section.label">
5945
</span>
6046
<span class="hidden lg:inline whitespace-nowrap">{{section.label}}</span>
6147
</a>
48+
<div class="flex items-center">
49+
<button type="button" aria-label="Search" @click="showPalette=!showPalette"
50+
class="flex items-center gap-1 rounded-full bg-gray-50 px-2 py-1 ring-1 ring-gray-200 hover:ring-green-500 text-gray-400 hover:text-gray-600">
51+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon" class="-ml-0.5 size-4 fill-gray-400 hover:fill-gray-600"><path fill-rule="evenodd" d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z" clip-rule="evenodd"></path></svg>
52+
<span class="text-sm">Search</span>
53+
<span class="hidden md:block text-gray-400 text-sm leading-5 py-0 px-1.5 mr-1.5 border border-gray-300 border-solid rounded-md" style="opacity: 1;">
54+
<span class="sr-only">Press </span>
55+
<kbd class="font-sans">/</kbd>
56+
<span class="sr-only"> to search</span>
57+
</span>
58+
</button>
59+
</div>
6260
</div>
6361
</div>
6462
<div class="hidden sm:ml-6 sm:flex sm:items-center">
@@ -81,17 +79,17 @@ export default {
8179
<SecondaryButton @click="routes.to({ admin:'SignIn' })">Sign In</SecondaryButton>
8280
</div>
8381
</div>
84-
<div class="-mr-2 flex items-center sm:hidden">
82+
<div @click="showMobileMenu=!showMobileMenu" class="-mr-2 flex items-center sm:hidden">
8583
<!-- Mobile menu button -->
8684
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" aria-controls="mobile-menu" aria-expanded="false">
8785
<span class="absolute -inset-0.5"></span>
8886
<span class="sr-only">Open main menu</span>
8987
<!-- Menu open: "hidden", Menu closed: "block" -->
90-
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
88+
<svg :class="showMobileMenu ? 'hidden' : 'block h-6 w-6'" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
9189
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
9290
</svg>
9391
<!-- Menu open: "block", Menu closed: "hidden" -->
94-
<svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
92+
<svg :class="showMobileMenu ? 'block h-6 w-6': 'hidden'" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
9593
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
9694
</svg>
9795
</button>
@@ -100,7 +98,7 @@ export default {
10098
</div>
10199
102100
<!-- Mobile menu, show/hide based on menu state. -->
103-
<div v-if="user" class="sm:hidden" id="mobile-menu">
101+
<div v-if="user && showMobileMenu" class="sm:hidden" id="mobile-menu">
104102
<div class="space-y-1 pb-3 pt-2">
105103
<!-- Current: "border-indigo-500 bg-indigo-50 text-indigo-700", Default: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800" -->
106104
<a v-href="{admin:section.id,id:undefined}" v-for="section in sections"
@@ -134,7 +132,31 @@ export default {
134132
<div class="mx-auto max-w-7xl pb-8 lg:px-6 lg:px-8">
135133
<SignIn v-if="routes.admin=='SignIn'" />
136134
<SignInForm v-else-if="routes.admin && !user" />
137-
<component v-else :key="refreshKey" :is="activeSection.component"></component>
135+
<div v-else :key="refreshKey">
136+
<div v-if="activeFeature?.features?.length > 0" class="border-b py-2">
137+
<div class="grid grid-cols-1 sm:hidden">
138+
<!-- Use an "onChange" listener to redirect the user to the selected tab URL. -->
139+
<select aria-label="Select a tab" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-2 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
140+
@change="routes.to({ admin:$event.target.value,id:undefined })">
141+
<option v-for="feature in activeFeature.features" :value="feature.id" :selected="feature.id==routes.admin">{{feature.label}}</option>
142+
</select>
143+
<svg class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end fill-gray-500" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon">
144+
<path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
145+
</svg>
146+
</div>
147+
<div class="hidden sm:block">
148+
<nav class="flex space-x-4" aria-label="Tabs">
149+
<!-- Current: "bg-indigo-100 text-indigo-700", Default: "text-gray-500 hover:text-gray-700" -->
150+
<a v-for="feature in activeFeature.features" v-href="{admin:feature.id,id:undefined}"
151+
:class="['rounded-md px-3 py-2 text-sm font-medium',
152+
feature.id==routes.admin ? 'bg-indigo-100 text-indigo-700' : 'text-gray-500 hover:text-gray-700']">
153+
{{feature.label}}
154+
</a>
155+
</nav>
156+
</div>
157+
</div>
158+
<component :is="activeSection.component"></component>
159+
</div>
138160
</div>
139161
</main>
140162
</div>
@@ -147,22 +169,17 @@ export default {
147169
const { user, hasRole, signIn, signOut } = useAuth()
148170
const profileUrl = ref(localStorage.getItem('profileUrl') || user.value?.profileUrl)
149171
const refreshKey = ref(1)
172+
const showMobileMenu = ref(false)
150173
const showUserMenu = ref(false)
174+
const showPalette = ref(false)
151175

152-
const sections = Object.keys(components).map(id => ({
153-
id,
154-
label: uiLabel(id),
155-
component: components[id],
156-
icon: icons[prefixes[id]],
157-
prefix: prefixes[id],
158-
}))
176+
const sections = Object.values(Features)
159177

160178
const overrides = {
161179
ImageUpscale: {
162180
label: 'Upscale',
163181
},
164182
}
165-
166183
Object.keys(overrides).forEach(id => {
167184
const section = sections.find(x => x.id === id)
168185
if (section) {
@@ -173,21 +190,36 @@ export default {
173190
}
174191
})
175192

176-
const activeSection = shallowRef(sections[routes.admin] || HomeSection)
193+
const activeSection = shallowRef(sections.find(x => x.id === routes.admin) || HomeSection)
194+
const activeFeature = shallowRef(FeatureGroups.find(f => f.features.some(p => p.id === routes.admin)))
177195

178196
function navTo(id, args, pushState=true) {
179197
if (!args) args = {}
180198

181199
refreshKey.value++
182200
activeSection.value = sections.find(x => x.id === id) || HomeSection
201+
activeFeature.value = FeatureGroups.find(f => f.features.some(p => p.id === routes.admin))
183202
routes.to({ admin: id, ...args })
184203
}
185204
watch(() => routes.admin, () => {
186205
activeSection.value = sections.find(x => x.id === routes.admin) || HomeSection
206+
activeFeature.value = FeatureGroups.find(f => f.features.some(p => p.id === routes.admin))
187207
if (!profileUrl.value) profileUrl.value = localStorage.getItem('profileUrl') || user.value?.profileUrl
188208
refreshKey.value++
189209
})
190210

211+
function handleKeyDown(e) {
212+
//console.log('handleKeyDown', e)
213+
if (e.code === 'Slash') {
214+
showPalette.value = true
215+
e.preventDefault()
216+
}
217+
if (e.code === 'Escape') {
218+
showPalette.value = false
219+
e.preventDefault()
220+
}
221+
}
222+
191223
onMounted(async () => {
192224
const api = await client.api(new Authenticate())
193225
if (api.response) {
@@ -199,11 +231,19 @@ export default {
199231

200232
console.log('routes.admin', routes.admin)
201233

234+
window.addEventListener('keydown', handleKeyDown)
235+
202236
nextTick(() =>
203237
activeSection.value = sections.find(x => x.id === routes.admin) || HomeSection)
204238
})
205239

206-
return { routes, user, hasRole, sections, activeSection, profileUrl, refreshKey, showUserMenu,
240+
onUnmounted(() => {
241+
window.removeEventListener('keydown', handleKeyDown)
242+
})
243+
244+
return { refreshKey, routes, user, hasRole, profileUrl,
245+
FeatureGroups, sections, activeSection, activeFeature,
246+
showMobileMenu, showUserMenu, showPalette,
207247
icons, navTo,
208248
}
209249
}

AiServer/wwwroot/admin/index.html

Lines changed: 20 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,7 @@ <h1 v-if="activeSection.title" class="hidden lg:block pt-4 mb-2 text-3xl font-bo
136136
import ServiceStackVue, { useClient, useAuth, useMetadata, useFormatters, useUtils, useConfig, useFiles } from "@servicestack/vue"
137137
import { Authenticate, AdminData, GetWorkerStats, GetSummaryStats } from "dtos"
138138
import { icons, iconDataUri } from "/mjs/utils.mjs"
139-
import Chat from "/mjs/components/Chat.mjs"
140-
import TextToImage from "/mjs/components/TextToImage.mjs"
141-
import ImageToText from "/mjs/components/ImageToText.mjs"
142-
import ImageToImage from "/mjs/components/ImageToImage.mjs"
143-
import ImageUpscale from "/mjs/components/ImageUpscale.mjs"
144-
import SpeechToText from "/mjs/components/SpeechToText.mjs"
145-
import TextToSpeech from "/mjs/components/TextToSpeech.mjs"
146-
import Transform from "/mjs/components/Transform.mjs"
139+
import { App, usePageRoutes, useBreakpoints, setBodyClass, sortOps } from "core"
147140

148141
import AiProviders from "/mjs/components/AiProviders.mjs"
149142
import CopyIcon from "/js/components/CopyIcon.mjs"
@@ -155,10 +148,10 @@ <h1 v-if="activeSection.title" class="hidden lg:block pt-4 mb-2 text-3xl font-bo
155148
import { ApiKeys } from "/modules/admin-ui/components/ApiKeys.mjs"
156149
import { CreateApiKeyForm, EditApiKeyForm } from "/modules/admin-ui/components/ManageUserApiKeys.mjs"
157150
import { ApiKeyDialog } from "/js/components/ApiKeyDialog.mjs"
158-
import { App, usePageRoutes, useBreakpoints, setBodyClass, sortOps } from "core"
159151
import metadata from "/modules/admin-ui/lib/metadata.mjs"
160152
import MediaProviders from "/mjs/components/MediaProviders.mjs"
161153
import SignInForm from "/mjs/components/SignInForm.mjs"
154+
import { Features } from "/mjs/components/Features.mjs"
162155

163156
if (location.search.includes('clear=metadata')) {
164157
localStorage.removeItem('/metadata/app.json')
@@ -209,6 +202,16 @@ <h1 v-if="activeSection.title" class="hidden lg:block pt-4 mb-2 text-3xl font-bo
209202
<dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900">{{formatNumber(stat.total)}}</dd>
210203
</div>
211204
</dl>
205+
<div class="mt-8 text-sm font-semibold leading-6">
206+
<div class="flex gap-x-2">
207+
<div class="mr-2">Go to <span aria-hidden="true">&rarr;</span></div>
208+
<a href="/" class="text-indigo-600 dark:text-indigo-300">Home</a>
209+
<svg class="h-5 w-5 flex-shrink-0 text-gray-300" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z"></path></svg>
210+
<a href="/ui/" class="text-indigo-600 dark:text-indigo-300">API Explorer</a>
211+
<svg class="h-5 w-5 flex-shrink-0 text-gray-300" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z"></path></svg>
212+
<a href="/admin-ui/" class="text-indigo-600 dark:text-indigo-300">Admin UI</a>
213+
</div>
214+
</div>
212215
<div class="pt-4">
213216
<h2 class="lg:block pt-4 mb-2 text-3xl font-bold leading-tight tracking-tight text-gray-900">Workers</h2>
214217
<div v-if="workerStats.length">
@@ -267,53 +270,14 @@ <h4 class="mt-4 font-semibold text-gray-500">By Month</h4>
267270
}
268271
},
269272
},
270-
Chat: {
271-
title: '',
272-
component: Chat,
273-
icon: icons.chat,
274-
},
275-
TextToImage: {
276-
title: '',
277-
label: 'Text to Image',
278-
component: TextToImage,
279-
icon: icons.txt2img,
280-
},
281-
ImageToText: {
282-
title: '',
283-
label: 'Image to Text',
284-
component: ImageToText,
285-
icon: icons.img2txt,
286-
},
287-
ImageToImage: {
288-
title: '',
289-
label: 'Image to Image',
290-
component: ImageToImage,
291-
icon: icons.img2img,
292-
},
293-
ImageUpscale: {
294-
title: '',
295-
label: 'Image Upscale',
296-
component: ImageUpscale,
297-
icon: icons.upscale,
298-
},
299-
SpeechToText: {
300-
title: '',
301-
label: 'Speech to Text',
302-
component: SpeechToText,
303-
icon: icons.spch2txt,
304-
},
305-
TextToSpeech: {
306-
title: '',
307-
label: 'Text to Speech',
308-
component: TextToSpeech,
309-
icon: icons.txt2spch,
310-
},
311-
// Transform: {
312-
// title: '',
313-
// label: 'Media Transform',
314-
// component: Transform,
315-
// icon: icons.ffmpeg,
316-
// },
273+
...(() => {
274+
const ret = {}
275+
Object.values(Features).forEach(x => {
276+
ret[x.id] = x
277+
ret[x.id].title = ''
278+
})
279+
return ret
280+
})(),
317281
AiProvider: {
318282
group: 'AI',
319283
label: 'AI Providers',

0 commit comments

Comments
 (0)