Download frontend/src/ui/studio/components/ModelSelector.jsx from ruslanmv/HomePilot: direct link, hf CLI and curl.
- Browser
- Download file 8.29 kB
-
https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/studio/components/ModelSelector.jsx
- Command line
-
hf download hf://spaces/ruslanmv/HomePilot/frontend/src/ui/studio/components/ModelSelector.jsx
-
curl -L -o ModelSelector.jsx https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/studio/components/ModelSelector.jsx
8.29 kB
| import React, { useState } from 'react'; | |
| /** | |
| * Model Selector | |
| * | |
| * Grid/list of available models with filtering. | |
| * Shows NSFW badges and download status. | |
| */ | |
| export const ModelSelector = ({ models, selectedModelId, onSelect, contentRating, showDownloadStatus = true, }) => { | |
| const [filter, setFilter] = useState('all'); | |
| const [searchQuery, setSearchQuery] = useState(''); | |
| // Filter models based on content rating and user filters | |
| const filteredModels = models.filter((model) => { | |
| // Hide NSFW models in SFW mode | |
| if (contentRating === 'sfw' && model.nsfw) { | |
| return false; | |
| } | |
| // Apply user filter | |
| if (filter === 'anime' && !model.anime) | |
| return false; | |
| if (filter === 'nsfw' && !model.nsfw) | |
| return false; | |
| if (filter === 'sfw' && model.nsfw) | |
| return false; | |
| // Apply search | |
| if (searchQuery) { | |
| const q = searchQuery.toLowerCase(); | |
| return (model.label.toLowerCase().includes(q) || | |
| model.description.toLowerCase().includes(q)); | |
| } | |
| return true; | |
| }); | |
| return (<div className="model-selector"> | |
| {/* Filters */} | |
| <div className="filters"> | |
| <input type="text" className="search-input" placeholder="Search models..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)}/> | |
| <div className="filter-pills"> | |
| {['all', 'anime', 'nsfw', 'sfw'].map((f) => (<button key={f} className={`filter-pill ${filter === f ? 'active' : ''}`} onClick={() => setFilter(f)} disabled={f === 'nsfw' && contentRating === 'sfw'}> | |
| {f === 'all' && 'All'} | |
| {f === 'anime' && '\uD83C\uDF38 Anime'} | |
| {f === 'nsfw' && '\uD83D\uDD1E NSFW'} | |
| {f === 'sfw' && '\u2713 SFW'} | |
| </button>))} | |
| </div> | |
| </div> | |
| {/* Model Grid */} | |
| <div className="model-grid"> | |
| {filteredModels.map((model) => (<div key={model.id} className={`model-card ${selectedModelId === model.id ? 'selected' : ''}`} onClick={() => onSelect(model.id)}> | |
| <div className="model-preview"> | |
| <div className="model-icon"> | |
| {model.anime ? '\uD83C\uDF38' : '\uD83D\uDDBC\uFE0F'} | |
| </div> | |
| </div> | |
| <div className="model-info"> | |
| <h4 className="model-name">{model.label}</h4> | |
| <p className="model-description">{model.description}</p> | |
| <div className="model-meta"> | |
| <span className="model-size">{model.size_gb} GB</span> | |
| {model.resolution && (<span className="model-resolution">{model.resolution}</span>)} | |
| </div> | |
| <div className="model-badges"> | |
| {model.nsfw ? (<span className="badge badge-nsfw">\uD83D\uDD1E NSFW</span>) : (<span className="badge badge-sfw">\u2713 SFW</span>)} | |
| {model.recommended_nsfw && (<span className="badge badge-recommended">\u2B50 Recommended</span>)} | |
| {model.anime && (<span className="badge badge-anime">Anime</span>)} | |
| </div> | |
| </div> | |
| {showDownloadStatus && (<div className="download-status"> | |
| {model.downloaded ? (<span className="status-ready">\u2713 Ready</span>) : (<button className="download-btn">Download</button>)} | |
| </div>)} | |
| </div>))} | |
| {filteredModels.length === 0 && (<div className="no-results"> | |
| <p>No models match your filters</p> | |
| {contentRating === 'sfw' && filter === 'nsfw' && (<p className="hint">Enable Mature mode to see NSFW models</p>)} | |
| </div>)} | |
| </div> | |
| <style>{` | |
| .model-selector { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .filters { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .search-input { | |
| width: 100%; | |
| padding: 10px 14px; | |
| border: 1px solid var(--color-border); | |
| border-radius: 8px; | |
| background: var(--color-background); | |
| color: var(--color-text); | |
| font-size: 14px; | |
| } | |
| .search-input:focus { | |
| outline: none; | |
| border-color: var(--color-primary); | |
| } | |
| .filter-pills { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .filter-pill { | |
| padding: 6px 14px; | |
| border: 1px solid var(--color-border); | |
| border-radius: 20px; | |
| background: var(--color-surface); | |
| color: var(--color-text-muted); | |
| font-size: 13px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .filter-pill:hover:not(:disabled) { | |
| border-color: var(--color-primary); | |
| color: var(--color-primary); | |
| } | |
| .filter-pill.active { | |
| background: var(--color-primary); | |
| border-color: var(--color-primary); | |
| color: white; | |
| } | |
| .filter-pill:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .model-grid { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .model-card { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 14px; | |
| padding: 14px; | |
| background: var(--color-surface); | |
| border: 2px solid var(--color-border); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .model-card:hover { | |
| border-color: var(--color-primary); | |
| } | |
| .model-card.selected { | |
| border-color: var(--color-primary); | |
| background: var(--color-primary-light); | |
| } | |
| .model-preview { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 8px; | |
| background: var(--color-background); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .model-icon { | |
| font-size: 28px; | |
| } | |
| .model-info { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .model-name { | |
| margin: 0 0 4px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--color-text); | |
| } | |
| .model-description { | |
| margin: 0 0 8px; | |
| font-size: 12px; | |
| color: var(--color-text-muted); | |
| line-height: 1.4; | |
| display: -webkit-box; | |
| -webkit-line-clamp: 2; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| .model-meta { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| font-size: 11px; | |
| color: var(--color-text-light); | |
| } | |
| .model-badges { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .badge { | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| font-weight: 500; | |
| } | |
| .badge-nsfw { | |
| background: linear-gradient(135deg, #8B5CF6, #F59E0B); | |
| color: white; | |
| } | |
| .badge-sfw { | |
| background: #D1FAE5; | |
| color: #065F46; | |
| } | |
| .badge-recommended { | |
| background: #FEF3C7; | |
| color: #92400E; | |
| } | |
| .badge-anime { | |
| background: #FCE7F3; | |
| color: #9D174D; | |
| } | |
| .download-status { | |
| flex-shrink: 0; | |
| } | |
| .status-ready { | |
| color: #10B981; | |
| font-size: 12px; | |
| font-weight: 500; | |
| } | |
| .download-btn { | |
| padding: 6px 12px; | |
| background: var(--color-primary); | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| cursor: pointer; | |
| } | |
| .download-btn:hover { | |
| background: var(--color-primary-hover); | |
| } | |
| .no-results { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: var(--color-text-muted); | |
| } | |
| .no-results .hint { | |
| font-size: 13px; | |
| color: var(--color-text-light); | |
| margin-top: 8px; | |
| } | |
| `}</style> | |
| </div>); | |
| }; | |