mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-18 13:49:09 -04:00
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:
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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와 대화하여 모델을 설치하고, 백엔드를 전환하고, 구성을 편집하고, 상태를 확인하세요.",
|
||||
|
||||
@@ -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 对话来安装模型、切换后端、编辑配置和查看状态。",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user