Spaces:
Running
Running
Download src/components/ModelPicker.vue from lrigamai/browser: direct link, hf CLI and curl.
- Browser
- Download file 5.38 kB
-
https://huggingface.co/spaces/lrigamai/browser/resolve/main/src/components/ModelPicker.vue
- Command line
-
hf download hf://spaces/lrigamai/browser/src/components/ModelPicker.vue
-
curl -L -o ModelPicker.vue https://huggingface.co/spaces/lrigamai/browser/resolve/main/src/components/ModelPicker.vue
5.38 kB
| <script setup lang="ts"> | |
| import { computed, onBeforeUnmount, onMounted, ref } from "vue"; | |
| import type { BaseModel } from "../types"; | |
| import { modelLabel, pad2 } from "../lib/format"; | |
| import Icon from "./Icon.vue"; | |
| /** | |
| * Base-model filter: a button summarising the selection, opening a checklist. Any combination can | |
| * be ticked and stays as ticked; "All models" (nothing ticked) clears it. `total` = number of characters. | |
| */ | |
| const props = defineProps<{ models: BaseModel[]; total: number }>(); | |
| const picked = defineModel<string[]>({ required: true }); | |
| const open = ref(false); | |
| const root = ref<HTMLElement>(); | |
| const summary = computed(() => { | |
| if (!picked.value.length) return "All models"; | |
| if (picked.value.length > 2) return `${picked.value.length} models`; | |
| return picked.value.map(modelLabel).join(" + "); | |
| }); | |
| function toggle(id: string) { | |
| const next = picked.value.includes(id) ? picked.value.filter((m) => m !== id) : [...picked.value, id]; | |
| // Keep base-model order. | |
| picked.value = props.models.map((m) => m.id).filter((m) => next.includes(m)); | |
| } | |
| // Closes on a click outside or Esc. | |
| function onDoc(e: Event) { | |
| if (open.value && !root.value?.contains(e.target as Node)) open.value = false; | |
| } | |
| function onKey(e: KeyboardEvent) { | |
| if (e.key === "Escape" && open.value) { | |
| open.value = false; | |
| root.value?.querySelector<HTMLElement>(".trigger")?.focus(); | |
| } | |
| } | |
| onMounted(() => { | |
| document.addEventListener("pointerdown", onDoc); | |
| document.addEventListener("keydown", onKey); | |
| }); | |
| onBeforeUnmount(() => { | |
| document.removeEventListener("pointerdown", onDoc); | |
| document.removeEventListener("keydown", onKey); | |
| }); | |
| </script> | |
| <template> | |
| <div ref="root" class="picker"> | |
| <button class="trigger mono" type="button" :aria-expanded="open" aria-haspopup="true" @click="open = !open"> | |
| <span class="k">Models</span> | |
| <span class="v" :class="{ on: picked.length }">{{ summary }}</span> | |
| <Icon name="chevron" class="caret" /> | |
| </button> | |
| <Transition name="pop"> | |
| <div v-if="open" class="menu" role="group" aria-label="Filter by base model"> | |
| <button class="row all" type="button" role="menuitemcheckbox" :aria-checked="!picked.length" @click="picked = []"> | |
| <span class="box" aria-hidden="true"><Icon v-if="!picked.length" name="check" /></span> | |
| <span class="name">All models</span> | |
| <span class="n mono">{{ pad2(total) }}</span> | |
| </button> | |
| <button | |
| v-for="m in models" | |
| :key="m.id" | |
| class="row" | |
| type="button" | |
| role="menuitemcheckbox" | |
| :aria-checked="picked.includes(m.id)" | |
| @click="toggle(m.id)" | |
| > | |
| <span class="box" aria-hidden="true"><Icon v-if="picked.includes(m.id)" name="check" /></span> | |
| <span class="name">{{ m.id }}</span> | |
| <span class="n mono">{{ pad2(m.count) }}</span> | |
| </button> | |
| </div> | |
| </Transition> | |
| </div> | |
| </template> | |
| <style scoped> | |
| .picker { | |
| position: relative; | |
| } | |
| /* fixed width: the summary changes as models are ticked, and the button mustn't resize with it */ | |
| .trigger { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| width: 220px; | |
| height: 32px; | |
| padding: 0 10px 0 12px; | |
| border: 1px solid var(--line-strong); | |
| border-radius: var(--radius); | |
| background: none; | |
| font-size: 12px; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| white-space: nowrap; | |
| cursor: pointer; | |
| transition: border-color 0.15s; | |
| } | |
| .trigger:hover, | |
| .trigger[aria-expanded="true"] { | |
| border-color: var(--text-3); | |
| } | |
| .k { | |
| font-size: 10px; | |
| color: var(--text-3); | |
| } | |
| .v { | |
| flex: 1; | |
| overflow: hidden; | |
| color: var(--text); | |
| text-align: left; | |
| text-overflow: ellipsis; | |
| } | |
| .v.on { | |
| color: var(--accent); | |
| } | |
| .caret { | |
| color: var(--text-3); | |
| transform: rotate(90deg); | |
| transition: transform 0.2s var(--ease); | |
| } | |
| .trigger[aria-expanded="true"] .caret { | |
| transform: rotate(-90deg); | |
| } | |
| .menu { | |
| position: absolute; | |
| top: calc(100% + 6px); | |
| right: 0; | |
| z-index: 10; | |
| width: 100%; | |
| min-width: 240px; | |
| padding: 4px; | |
| border: 1px solid var(--line-strong); | |
| border-radius: var(--radius); | |
| background: var(--surface); | |
| box-shadow: 0 12px 40px rgb(0 0 0 / 0.55); | |
| } | |
| .row { | |
| display: grid; | |
| grid-template-columns: 16px 1fr auto; | |
| align-items: center; | |
| gap: 10px; | |
| width: 100%; | |
| height: 36px; | |
| padding: 0 10px; | |
| border: 0; | |
| border-radius: 2px; | |
| background: none; | |
| text-align: left; | |
| cursor: pointer; | |
| } | |
| .row:hover { | |
| background: var(--surface-2); | |
| } | |
| .all { | |
| margin-bottom: 4px; | |
| border-bottom: 1px solid var(--line); | |
| border-radius: 2px 2px 0 0; | |
| } | |
| .box { | |
| display: grid; | |
| place-items: center; | |
| width: 16px; | |
| height: 16px; | |
| border: 1px solid var(--line-strong); | |
| border-radius: 2px; | |
| font-size: 12px; | |
| color: var(--accent-ink); | |
| } | |
| [aria-checked="true"] .box { | |
| border-color: var(--accent); | |
| background: var(--accent); | |
| } | |
| .name { | |
| overflow: hidden; | |
| font-family: var(--font-display); | |
| font-size: 18px; | |
| font-weight: 800; | |
| letter-spacing: 0.01em; | |
| text-transform: uppercase; | |
| color: var(--text-2); | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| [aria-checked="true"] .name { | |
| color: var(--text); | |
| } | |
| .n { | |
| font-size: 10px; | |
| color: var(--text-3); | |
| } | |
| [aria-checked="true"] .n { | |
| color: var(--accent); | |
| } | |
| .pop-enter-active, | |
| .pop-leave-active { | |
| transition: opacity 0.15s, transform 0.15s var(--ease); | |
| } | |
| .pop-enter-from, | |
| .pop-leave-to { | |
| opacity: 0; | |
| transform: translateY(-4px); | |
| } | |
| /* phones: the label goes, the summary stays */ | |
| @media (max-width: 560px) { | |
| .trigger { | |
| width: 150px; | |
| } | |
| .k { | |
| display: none; | |
| } | |
| } | |
| </style> | |