feat(randomizer): add lora pool Vue widget

This commit is contained in:
Will Miao
2026-01-11 16:26:38 +08:00
parent 32249d1886
commit 3d348900ac
26 changed files with 4658 additions and 119 deletions
@@ -0,0 +1,87 @@
<template>
<div class="section">
<div class="section__header">
<span class="section__title">BASE MODEL</span>
<EditButton @click="$emit('edit')" />
</div>
<div class="section__content">
<div v-if="selected.length === 0" class="section__placeholder">
All models
</div>
<div v-else class="section__chips">
<FilterChip
v-for="name in selected"
:key="name"
:label="name"
:count="getCount(name)"
variant="neutral"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import FilterChip from '../shared/FilterChip.vue'
import EditButton from '../shared/EditButton.vue'
import type { BaseModelOption } from '../../../composables/types'
const props = defineProps<{
selected: string[]
models: BaseModelOption[]
}>()
defineEmits<{
edit: []
}>()
const getCount = (name: string) => {
const model = props.models.find(m => m.name === name)
return model?.count
}
</script>
<style scoped>
.section {
margin-bottom: 16px;
}
.section__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.section__title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-color, #fff);
opacity: 0.6;
}
.section__content {
min-height: 32px;
display: flex;
align-items: center;
}
.section__placeholder {
width: 100%;
padding: 8px 12px;
background: var(--comfy-input-bg, #333);
border-radius: 4px;
font-size: 12px;
color: var(--fg-color, #fff);
opacity: 0.5;
text-align: center;
}
.section__chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
</style>
@@ -0,0 +1,234 @@
<template>
<div class="section">
<div class="section__header">
<span class="section__title">FOLDERS</span>
</div>
<div class="section__columns">
<!-- Include column -->
<div class="section__column">
<div class="section__column-header">
<span class="section__column-title section__column-title--include">INCLUDE</span>
</div>
<div class="section__input-row">
<input
v-model="includeInput"
type="text"
class="section__input"
placeholder="Path..."
@keydown.enter="addInclude"
/>
<button
type="button"
class="section__add-btn section__add-btn--include"
@click="addInclude"
>
+
</button>
</div>
<div v-if="includeFolders.length > 0" class="section__paths">
<FilterChip
v-for="path in includeFolders"
:key="path"
:label="truncatePath(path)"
variant="path"
removable
@remove="removeInclude(path)"
/>
</div>
</div>
<!-- Exclude column -->
<div class="section__column">
<div class="section__column-header">
<span class="section__column-title section__column-title--exclude">EXCLUDE</span>
</div>
<div class="section__input-row">
<input
v-model="excludeInput"
type="text"
class="section__input"
placeholder="Path..."
@keydown.enter="addExclude"
/>
<button
type="button"
class="section__add-btn section__add-btn--exclude"
@click="addExclude"
>
+
</button>
</div>
<div v-if="excludeFolders.length > 0" class="section__paths">
<FilterChip
v-for="path in excludeFolders"
:key="path"
:label="truncatePath(path)"
variant="path"
removable
@remove="removeExclude(path)"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import FilterChip from '../shared/FilterChip.vue'
const props = defineProps<{
includeFolders: string[]
excludeFolders: string[]
}>()
const emit = defineEmits<{
'update:includeFolders': [value: string[]]
'update:excludeFolders': [value: string[]]
}>()
const includeInput = ref('')
const excludeInput = ref('')
const truncatePath = (path: string) => {
if (path.length <= 20) return path
return '...' + path.slice(-17)
}
const addInclude = () => {
const path = includeInput.value.trim()
if (path && !props.includeFolders.includes(path)) {
emit('update:includeFolders', [...props.includeFolders, path])
includeInput.value = ''
}
}
const removeInclude = (path: string) => {
emit('update:includeFolders', props.includeFolders.filter(p => p !== path))
}
const addExclude = () => {
const path = excludeInput.value.trim()
if (path && !props.excludeFolders.includes(path)) {
emit('update:excludeFolders', [...props.excludeFolders, path])
excludeInput.value = ''
}
}
const removeExclude = (path: string) => {
emit('update:excludeFolders', props.excludeFolders.filter(p => p !== path))
}
</script>
<style scoped>
.section {
margin-bottom: 16px;
}
.section__header {
margin-bottom: 8px;
}
.section__title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-color, #fff);
opacity: 0.6;
}
.section__columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.section__column {
min-width: 0;
}
.section__column-header {
margin-bottom: 6px;
}
.section__column-title {
font-size: 9px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.section__column-title--include {
color: #22c55e;
}
.section__column-title--exclude {
color: #ef4444;
}
.section__input-row {
display: flex;
gap: 4px;
}
.section__input {
flex: 1;
min-width: 0;
padding: 6px 8px;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 4px;
color: var(--fg-color, #fff);
font-size: 11px;
outline: none;
}
.section__input:focus {
border-color: var(--fg-color, #fff);
}
.section__input::placeholder {
color: var(--fg-color, #fff);
opacity: 0.4;
}
.section__add-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 4px;
color: var(--fg-color, #fff);
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.section__add-btn:hover {
border-color: var(--fg-color, #fff);
}
.section__add-btn--include:hover {
background: rgba(34, 197, 94, 0.2);
border-color: #22c55e;
color: #22c55e;
}
.section__add-btn--exclude:hover {
background: rgba(239, 68, 68, 0.2);
border-color: #ef4444;
color: #ef4444;
}
.section__paths {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
</style>
@@ -0,0 +1,132 @@
<template>
<div class="section">
<div class="section__header">
<span class="section__title">LICENSE</span>
</div>
<div class="section__toggles">
<label class="toggle-item">
<span class="toggle-item__label">No Credit</span>
<button
type="button"
class="toggle-switch"
:class="{ 'toggle-switch--active': noCreditRequired }"
@click="$emit('update:noCreditRequired', !noCreditRequired)"
role="switch"
:aria-checked="noCreditRequired"
>
<span class="toggle-switch__track"></span>
<span class="toggle-switch__thumb"></span>
</button>
</label>
<label class="toggle-item">
<span class="toggle-item__label">Allow Selling</span>
<button
type="button"
class="toggle-switch"
:class="{ 'toggle-switch--active': allowSelling }"
@click="$emit('update:allowSelling', !allowSelling)"
role="switch"
:aria-checked="allowSelling"
>
<span class="toggle-switch__track"></span>
<span class="toggle-switch__thumb"></span>
</button>
</label>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
noCreditRequired: boolean
allowSelling: boolean
}>()
defineEmits<{
'update:noCreditRequired': [value: boolean]
'update:allowSelling': [value: boolean]
}>()
</script>
<style scoped>
.section {
margin-bottom: 16px;
}
.section__header {
margin-bottom: 8px;
}
.section__title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-color, #fff);
opacity: 0.6;
}
.section__toggles {
display: flex;
gap: 16px;
}
.toggle-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.toggle-item__label {
font-size: 12px;
color: var(--fg-color, #fff);
}
.toggle-switch {
position: relative;
width: 36px;
height: 20px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
}
.toggle-switch__track {
position: absolute;
inset: 0;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 10px;
transition: all 0.2s;
}
.toggle-switch--active .toggle-switch__track {
background: rgba(34, 197, 94, 0.3);
border-color: rgba(34, 197, 94, 0.6);
}
.toggle-switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
background: var(--fg-color, #fff);
border-radius: 50%;
transition: all 0.2s;
opacity: 0.6;
}
.toggle-switch--active .toggle-switch__thumb {
transform: translateX(16px);
background: #22c55e;
opacity: 1;
}
.toggle-switch:hover .toggle-switch__thumb {
opacity: 1;
}
</style>
@@ -0,0 +1,133 @@
<template>
<div class="section">
<div class="section__header">
<span class="section__title">TAGS</span>
</div>
<div class="section__columns">
<!-- Include column -->
<div class="section__column">
<div class="section__column-header">
<span class="section__column-title section__column-title--include">INCLUDE</span>
<EditButton @click="$emit('edit-include')" />
</div>
<div class="section__column-content">
<div v-if="includeTags.length === 0" class="section__empty">
None
</div>
<div v-else class="section__chips">
<FilterChip
v-for="tag in includeTags"
:key="tag"
:label="tag"
variant="include"
/>
</div>
</div>
</div>
<!-- Exclude column -->
<div class="section__column">
<div class="section__column-header">
<span class="section__column-title section__column-title--exclude">EXCLUDE</span>
<EditButton @click="$emit('edit-exclude')" />
</div>
<div class="section__column-content">
<div v-if="excludeTags.length === 0" class="section__empty">
None
</div>
<div v-else class="section__chips">
<FilterChip
v-for="tag in excludeTags"
:key="tag"
:label="tag"
variant="exclude"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import FilterChip from '../shared/FilterChip.vue'
import EditButton from '../shared/EditButton.vue'
defineProps<{
includeTags: string[]
excludeTags: string[]
}>()
defineEmits<{
'edit-include': []
'edit-exclude': []
}>()
</script>
<style scoped>
.section {
margin-bottom: 16px;
}
.section__header {
margin-bottom: 8px;
}
.section__title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-color, #fff);
opacity: 0.6;
}
.section__columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.section__column {
min-width: 0;
}
.section__column-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.section__column-title {
font-size: 9px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.section__column-title--include {
color: #22c55e;
}
.section__column-title--exclude {
color: #ef4444;
}
.section__column-content {
min-height: 28px;
}
.section__empty {
padding: 6px 0;
font-size: 11px;
color: var(--fg-color, #fff);
opacity: 0.4;
}
.section__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
</style>