feat(ui): Home editorial header + status line (north-star redesign)

Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2026-06-18 10:44:28 +00:00
parent 0838d7d3e6
commit c7d9dbda6b
9 changed files with 147 additions and 57 deletions

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Good morning",
"afternoon": "Good afternoon",
"evening": "Good evening",
"night": "Working late"
},
"statusLine": {
"modelsLoaded_one": "{{count}} model loaded",
"modelsLoaded_other": "{{count}} models loaded",
"noModelsLoaded": "No models loaded",
"nodes_one": "{{count}} node",
"nodes_other": "{{count}} nodes"
},
"assistant": {
"title": "LocalAI per Chat verwalten",
"description": "Modelle installieren, Backends wechseln, Konfigurationen bearbeiten und Status prüfen — durch Gespräche mit LocalAI.",

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Good morning",
"afternoon": "Good afternoon",
"evening": "Good evening",
"night": "Working late"
},
"statusLine": {
"modelsLoaded_one": "{{count}} model loaded",
"modelsLoaded_other": "{{count}} models loaded",
"noModelsLoaded": "No models loaded",
"nodes_one": "{{count}} node",
"nodes_other": "{{count}} nodes"
},
"assistant": {
"title": "Manage LocalAI by chatting",
"description": "Install models, switch backends, edit configs and check status by talking to LocalAI.",

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Good morning",
"afternoon": "Good afternoon",
"evening": "Good evening",
"night": "Working late"
},
"statusLine": {
"modelsLoaded_one": "{{count}} model loaded",
"modelsLoaded_other": "{{count}} models loaded",
"noModelsLoaded": "No models loaded",
"nodes_one": "{{count}} node",
"nodes_other": "{{count}} nodes"
},
"assistant": {
"title": "Administra LocalAI chateando",
"description": "Instala modelos, cambia backends, edita configuraciones y consulta el estado hablando con LocalAI.",

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Good morning",
"afternoon": "Good afternoon",
"evening": "Good evening",
"night": "Working late"
},
"statusLine": {
"modelsLoaded_one": "{{count}} model loaded",
"modelsLoaded_other": "{{count}} models loaded",
"noModelsLoaded": "No models loaded",
"nodes_one": "{{count}} node",
"nodes_other": "{{count}} nodes"
},
"assistant": {
"title": "Kelola LocalAI melalui obrolan",
"description": "Instal model, ganti backend, edit konfigurasi dan periksa status dengan berbicara pada LocalAI.",

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Buongiorno",
"afternoon": "Buon pomeriggio",
"evening": "Buonasera",
"night": "Al lavoro fino a tardi"
},
"statusLine": {
"modelsLoaded_one": "{{count}} modello caricato",
"modelsLoaded_other": "{{count}} modelli caricati",
"noModelsLoaded": "Nessun modello caricato",
"nodes_one": "{{count}} nodo",
"nodes_other": "{{count}} nodi"
},
"assistant": {
"title": "Gestisci LocalAI chattando",
"description": "Installa modelli, cambia backend, modifica configurazioni e controlla lo stato parlando con LocalAI.",

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Good morning",
"afternoon": "Good afternoon",
"evening": "Good evening",
"night": "Working late"
},
"statusLine": {
"modelsLoaded_one": "{{count}} model loaded",
"modelsLoaded_other": "{{count}} models loaded",
"noModelsLoaded": "No models loaded",
"nodes_one": "{{count}} node",
"nodes_other": "{{count}} nodes"
},
"assistant": {
"title": "채팅으로 LocalAI 관리",
"description": "LocalAI와 대화하여 모델을 설치하고, 백엔드를 전환하고, 구성을 편집하고, 상태를 확인하세요.",

View File

@@ -6,6 +6,19 @@
},
"resourceGpu": "GPU",
"resourceRam": "RAM",
"greeting": {
"morning": "Good morning",
"afternoon": "Good afternoon",
"evening": "Good evening",
"night": "Working late"
},
"statusLine": {
"modelsLoaded_one": "{{count}} model loaded",
"modelsLoaded_other": "{{count}} models loaded",
"noModelsLoaded": "No models loaded",
"nodes_one": "{{count}} node",
"nodes_other": "{{count}} nodes"
},
"assistant": {
"title": "通过聊天管理 LocalAI",
"description": "通过与 LocalAI 对话来安装模型、切换后端、编辑配置和查看状态。",

View File

@@ -5619,16 +5619,41 @@ select.input {
/* Home page */
.home-page {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 52rem;
gap: var(--space-section);
max-width: var(--page-max-medium);
margin: 0 auto;
padding: var(--spacing-2xl) var(--spacing-xl);
padding: var(--space-section) var(--spacing-lg);
width: 100%;
}
.home-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--spacing-lg);
flex-wrap: wrap;
}
.home-eyebrow {
font-family: var(--font-mono);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--color-eyebrow);
}
.home-greeting {
font-family: var(--font-serif);
font-size: clamp(2rem, 1.4rem + 3vw, var(--text-4xl));
font-weight: 440;
letter-spacing: -0.02em;
line-height: 1.05;
margin: var(--spacing-xs) 0 0;
}
.home-status-line {
display: flex;
align-items: center;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.home-hero {
text-align: center;

View File

@@ -12,12 +12,9 @@ import HomeConnect from '../components/HomeConnect'
import { useResources } from '../hooks/useResources'
import { fileToBase64, backendControlApi, systemApi, modelsApi, mcpApi, nodesApi } from '../utils/api'
import { API_CONFIG } from '../utils/config'
function formatBytes(bytes) {
if (!bytes || bytes === 0) return null
const gb = bytes / (1024 * 1024 * 1024)
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / (1024 * 1024)).toFixed(0)} MB`
}
import { greetingKey } from '../utils/greeting'
import StatusPill from '../components/StatusPill'
import { staggerStyle } from '../hooks/useStagger'
export default function Home() {
const navigate = useNavigate()
@@ -287,62 +284,39 @@ export default function Home() {
const hasModels = modelsLoading || configuredModels.length > 0
const loadedCount = loadedModels.length
// Resource display
// Resource display - folded into the editorial status line.
const resType = resources?.type
const usagePct = resources?.aggregate?.usage_percent ?? resources?.ram?.usage_percent ?? 0
const pctColor = usagePct > 90 ? 'var(--color-error)' : usagePct > 70 ? 'var(--color-warning)' : 'var(--color-success)'
// Cluster resource display (distributed mode)
const clusterUsagePct = clusterData?.totalMem > 0 ? ((clusterData.usedMem / clusterData.totalMem) * 100) : 0
const clusterPctColor = clusterUsagePct > 90 ? 'var(--color-error)' : clusterUsagePct > 70 ? 'var(--color-warning)' : 'var(--color-success)'
return (
<div className="home-page">
{hasModels ? (
<>
{/* Hero with logo */}
<div className="home-hero">
<img src={apiUrl(branding.logoUrl)} alt={branding.instanceName} className="home-logo" />
</div>
{/* Resource monitor - prominent placement */}
{distributedMode && clusterData && clusterData.totalMem > 0 ? (
<div className="home-resource-bar">
<div className="home-resource-bar-header">
<i className={`fas ${clusterData.isGPU ? 'fa-microchip' : 'fa-memory'}`} />
<span className="home-resource-label">{clusterData.isGPU ? t('cluster.vram') : t('cluster.ram')}</span>
<span className="home-resource-pct" style={{ color: clusterPctColor }}>
{formatBytes(clusterData.usedMem)} / {formatBytes(clusterData.totalMem)}
</span>
</div>
<div className="home-resource-track">
<div
className="home-resource-fill"
style={{ width: `${clusterUsagePct}%`, background: clusterPctColor }}
/>
</div>
<div className="home-cluster-status">
<span className="home-cluster-dot" style={clusterData.healthyCount === 0 ? { background: 'var(--color-error)' } : undefined} />
<span>{t('cluster.nodesOnline', { healthy: clusterData.healthyCount, total: clusterData.totalCount })}</span>
</div>
{/* Editorial header */}
<header className="home-header reveal-stagger">
<div style={staggerStyle(0)}>
<span className="home-eyebrow">{branding.instanceName}</span>
<h1 className="home-greeting">{t(`greeting.${greetingKey()}`)}</h1>
</div>
) : !distributedMode && resources ? (
<div className="home-resource-bar">
<div className="home-resource-bar-header">
<i className={`fas ${resType === 'gpu' ? 'fa-microchip' : 'fa-memory'}`} />
<span className="home-resource-label">{resType === 'gpu' ? t('resourceGpu') : t('resourceRam')}</span>
<span className="home-resource-pct" style={{ color: pctColor }}>
{usagePct.toFixed(0)}%
</span>
</div>
<div className="home-resource-track">
<div
className="home-resource-fill"
style={{ width: `${usagePct}%`, background: pctColor }}
<div className="home-status-line" style={staggerStyle(1)}>
<StatusPill
status={loadedCount > 0 ? 'healthy' : 'idle'}
label={loadedCount > 0 ? t('statusLine.modelsLoaded', { count: loadedCount }) : t('statusLine.noModelsLoaded')}
/>
{distributedMode && clusterData && (
<StatusPill
status={clusterData.healthyCount > 0 ? 'healthy' : 'error'}
label={t('statusLine.nodes', { count: clusterData.totalCount })}
/>
</div>
)}
{!distributedMode && resources && (
<span className="status-pill">
<i className={`fas ${resType === 'gpu' ? 'fa-microchip' : 'fa-memory'}`} aria-hidden="true" />
{(resType === 'gpu' ? t('resourceGpu') : t('resourceRam'))} {usagePct.toFixed(0)}%
</span>
)}
</div>
) : null}
</header>
{/* LocalAI Assistant — prominent CTA on first run. Once the
admin has used it, the big card collapses to a small entry in