mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-18 13:49:09 -04:00
refactor(ui): declutter Home - discoverable + dismissable API, vertical balance
Home felt overloaded and top-heavy. Three changes from review: - The API endpoint catalog (12 endpoints) is collapsed by default behind a "Browse the API" disclosure; only the base URL + copy stay visible, so the catalog is discoverable without dominating the page. - The whole connect card is dismissable (x): dismissing unmounts it so the vertical space is recovered, and the choice is remembered (localStorage). - .home-page now fills its column and vertically centers its content when there is slack, so sparse states (no models / card dismissed) read as a balanced launcher instead of content jammed at the top. Overflow-safe: tall content flows from the top and scrolls. Adds connect.browse / connect.hide / connect.dismiss i18n keys to all locales. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "Drop-in-Kompatibilität",
|
||||
"apiReference": "Vollständige API-Referenz",
|
||||
"copy": "Kopieren",
|
||||
"copied": "Kopiert"
|
||||
"copied": "Kopiert",
|
||||
"browse": "Browse the API",
|
||||
"hide": "Hide endpoints",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "Drop-in compatibility",
|
||||
"apiReference": "Full API reference",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
"copied": "Copied",
|
||||
"browse": "Browse the API",
|
||||
"hide": "Hide endpoints",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "Compatibilidad directa",
|
||||
"apiReference": "Referencia completa de la API",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado"
|
||||
"copied": "Copiado",
|
||||
"browse": "Browse the API",
|
||||
"hide": "Hide endpoints",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "Kompatibilitas drop-in",
|
||||
"apiReference": "Referensi API lengkap",
|
||||
"copy": "Salin",
|
||||
"copied": "Disalin"
|
||||
"copied": "Disalin",
|
||||
"browse": "Browse the API",
|
||||
"hide": "Hide endpoints",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "Compatibilità drop-in",
|
||||
"apiReference": "Riferimento API completo",
|
||||
"copy": "Copia",
|
||||
"copied": "Copiato"
|
||||
"copied": "Copiato",
|
||||
"browse": "Esplora le API",
|
||||
"hide": "Nascondi gli endpoint",
|
||||
"dismiss": "Ignora"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "드롭인 호환성",
|
||||
"apiReference": "전체 API 레퍼런스",
|
||||
"copy": "복사",
|
||||
"copied": "복사됨"
|
||||
"copied": "복사됨",
|
||||
"browse": "Browse the API",
|
||||
"hide": "Hide endpoints",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"compatTitle": "即插即用兼容",
|
||||
"apiReference": "完整 API 参考",
|
||||
"copy": "复制",
|
||||
"copied": "已复制"
|
||||
"copied": "已复制",
|
||||
"browse": "Browse the API",
|
||||
"hide": "Hide endpoints",
|
||||
"dismiss": "Dismiss"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5618,6 +5618,8 @@ select.input {
|
||||
.home-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
gap: var(--space-section);
|
||||
max-width: var(--page-max-medium);
|
||||
margin: 0 auto;
|
||||
@@ -6121,6 +6123,39 @@ select.input {
|
||||
padding: 0;
|
||||
}
|
||||
.home-connect-url .btn { flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.home-connect-dismiss {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: background var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
.home-connect-dismiss:hover { background: var(--color-surface-hover); color: var(--color-text-primary); }
|
||||
.home-connect-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-xs) 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-primary);
|
||||
font: inherit;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
}
|
||||
.home-connect-toggle:hover { color: var(--color-primary-hover); }
|
||||
.home-connect-toggle i { font-size: 0.7em; }
|
||||
.home-connect-endpoints { margin-top: var(--spacing-sm); }
|
||||
.home-connect-block { margin-top: var(--spacing-md); }
|
||||
.home-connect-block-head {
|
||||
display: flex;
|
||||
|
||||
@@ -27,6 +27,14 @@ const COMPAT = [
|
||||
export default function HomeConnect() {
|
||||
const { t } = useTranslation('home')
|
||||
const [copied, setCopied] = useState(false)
|
||||
// Endpoint catalog is collapsed by default so Home stays uncluttered; the
|
||||
// base URL stays visible and the full list is one click away (discoverable).
|
||||
const [showEndpoints, setShowEndpoints] = useState(false)
|
||||
// Dismissable: hiding the card unmounts it entirely so the vertical space is
|
||||
// recovered, and the choice is remembered across visits.
|
||||
const [dismissed, setDismissed] = useState(() => {
|
||||
try { return localStorage.getItem('localai_home_connect_dismissed') === '1' } catch { return false }
|
||||
})
|
||||
|
||||
// Absolute base for this instance, honouring any sub-path mount.
|
||||
const base = new URL(apiUrl('/'), window.location.origin).href.replace(/\/$/, '')
|
||||
@@ -39,6 +47,13 @@ export default function HomeConnect() {
|
||||
} catch (_) { /* clipboard blocked — the URL is selectable anyway */ }
|
||||
}
|
||||
|
||||
const dismiss = () => {
|
||||
try { localStorage.setItem('localai_home_connect_dismissed', '1') } catch { /* ignore */ }
|
||||
setDismissed(true)
|
||||
}
|
||||
|
||||
if (dismissed) return null
|
||||
|
||||
return (
|
||||
<section className="home-connect card" aria-labelledby="home-connect-title">
|
||||
<div className="home-connect-head">
|
||||
@@ -47,6 +62,9 @@ export default function HomeConnect() {
|
||||
<h2 id="home-connect-title" className="home-connect-title">{t('connect.title')}</h2>
|
||||
<p className="home-connect-sub">{t('connect.subtitle')}</p>
|
||||
</div>
|
||||
<button type="button" className="home-connect-dismiss" onClick={dismiss} aria-label={t('connect.dismiss')} title={t('connect.dismiss')}>
|
||||
<i className="fas fa-times" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="home-connect-url">
|
||||
@@ -57,6 +75,19 @@ export default function HomeConnect() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="home-connect-toggle"
|
||||
aria-expanded={showEndpoints}
|
||||
aria-controls="home-connect-endpoints"
|
||||
onClick={() => setShowEndpoints(v => !v)}
|
||||
>
|
||||
<i className={`fas fa-chevron-${showEndpoints ? 'up' : 'down'}`} aria-hidden="true" />
|
||||
<span>{showEndpoints ? t('connect.hide') : t('connect.browse')}</span>
|
||||
</button>
|
||||
|
||||
{showEndpoints && (
|
||||
<div id="home-connect-endpoints" className="home-connect-endpoints">
|
||||
<div className="home-connect-block">
|
||||
<div className="home-connect-block-head">
|
||||
<span className="home-connect-block-title">{t('connect.nativeTitle')}</span>
|
||||
@@ -87,6 +118,8 @@ export default function HomeConnect() {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user