mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Add real-time system stats (CPU/RAM/GPU/VRAM) and fix favicon
Favicon now matches the LokiMark topbar icon instead of an unrelated arrow glyph. New /api/system/stats endpoint (psutil + nvidia-smi) polled every 2s and shown in the topbar next to the Ollama status.
This commit is contained in:
1 parent
b9bccac4ba
commit
ecadf55743
8 files changed
+120
-7
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="10" fill="#18181b"/>
|
||||
<rect x="14" y="14" width="38" height="38" rx="7" fill="#ff5436"/>
|
||||
<rect x="11" y="11" width="38" height="38" rx="7" fill="#ff5436" stroke="#ffffff" stroke-width="6"/>
|
||||
<rect x="25" y="25" width="12" height="12" fill="#ffffff"/>
|
||||
<path d="M18 54h38V16" fill="none" stroke="#ff5436" stroke-width="5" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 358 B |
+11
-5
@@ -10,16 +10,22 @@ import { useStore } from "./store/useStore";
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<View>("chat");
|
||||
const { refreshStatus, refreshModels, refreshConfig } = useStore();
|
||||
const { refreshStatus, refreshSystemStats, refreshModels, refreshConfig } =
|
||||
useStore();
|
||||
|
||||
// Au démarrage : statut Ollama + modèles + config. Poll du statut.
|
||||
// Au démarrage : statut Ollama + modèles + config. Poll du statut et des stats système.
|
||||
useEffect(() => {
|
||||
refreshStatus();
|
||||
refreshSystemStats();
|
||||
refreshModels();
|
||||
refreshConfig();
|
||||
const id = setInterval(refreshStatus, 10000);
|
||||
return () => clearInterval(id);
|
||||
}, [refreshStatus, refreshModels, refreshConfig]);
|
||||
const statusId = setInterval(refreshStatus, 10000);
|
||||
const statsId = setInterval(refreshSystemStats, 2000);
|
||||
return () => {
|
||||
clearInterval(statusId);
|
||||
clearInterval(statsId);
|
||||
};
|
||||
}, [refreshStatus, refreshSystemStats, refreshModels, refreshConfig]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-base text-ink">
|
||||
|
||||
@@ -21,6 +21,26 @@ export async function getStatus(): Promise<OllamaStatus> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface GpuStats {
|
||||
name: string;
|
||||
util_pct: number;
|
||||
vram_used_mb: number;
|
||||
vram_total_mb: number;
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
cpu_pct: number;
|
||||
ram_used_go: number;
|
||||
ram_total_go: number;
|
||||
ram_pct: number;
|
||||
gpu: GpuStats | null;
|
||||
}
|
||||
|
||||
export async function getSystemStats(): Promise<SystemStats> {
|
||||
const res = await fetch("/api/system/stats");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getModels(): Promise<{
|
||||
models: OllamaModel[];
|
||||
default: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ModelSelector } from "./ModelSelector";
|
||||
/** Barre supérieure sombre : logo, fil d'Ariane, statut Ollama, sélecteur. */
|
||||
export function TopBar() {
|
||||
const status = useStore((s) => s.status);
|
||||
const stats = useStore((s) => s.systemStats);
|
||||
const connected = status?.connected ?? false;
|
||||
|
||||
return (
|
||||
@@ -24,6 +25,25 @@ export function TopBar() {
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Stats système temps réel : CPU, RAM, GPU/VRAM */}
|
||||
{stats && (
|
||||
<div className="flex h-8 items-center gap-2.5 border-[3px] border-chrome-3 bg-chrome-2 px-[11px] text-[13px] text-on-dark">
|
||||
<span>CPU {stats.cpu_pct.toFixed(0)}%</span>
|
||||
<span className="text-on-dark-3">·</span>
|
||||
<span>RAM {stats.ram_pct.toFixed(0)}%</span>
|
||||
{stats.gpu && (
|
||||
<>
|
||||
<span className="text-on-dark-3">·</span>
|
||||
<span>GPU {stats.gpu.util_pct.toFixed(0)}%</span>
|
||||
<span className="text-on-dark-3">
|
||||
VRAM {(stats.gpu.vram_used_mb / 1024).toFixed(1)}/
|
||||
{(stats.gpu.vram_total_mb / 1024).toFixed(1)}G
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Statut Ollama */}
|
||||
<div
|
||||
className="flex h-8 items-center gap-2 border-[3px] border-chrome-3 bg-chrome-2 px-[11px]"
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getModels,
|
||||
getSession,
|
||||
getStatus,
|
||||
getSystemStats,
|
||||
fileContent,
|
||||
listFiles,
|
||||
listSessions,
|
||||
@@ -18,11 +19,13 @@ import {
|
||||
type OllamaModel,
|
||||
type OllamaStatus,
|
||||
type Session,
|
||||
type SystemStats,
|
||||
type ToolCall,
|
||||
} from "../api/client";
|
||||
|
||||
interface LokiState {
|
||||
status: OllamaStatus | null;
|
||||
systemStats: SystemStats | null;
|
||||
models: OllamaModel[];
|
||||
selectedModel: string;
|
||||
loadingModels: boolean;
|
||||
@@ -54,6 +57,7 @@ interface LokiState {
|
||||
openPreview: (path: string) => Promise<void>;
|
||||
setSelectedModel: (name: string) => void;
|
||||
refreshStatus: () => Promise<void>;
|
||||
refreshSystemStats: () => Promise<void>;
|
||||
refreshModels: () => Promise<void>;
|
||||
refreshFiles: () => Promise<void>;
|
||||
|
||||
@@ -69,6 +73,7 @@ let activeStreamController: AbortController | null = null;
|
||||
|
||||
export const useStore = create<LokiState>((set, get) => ({
|
||||
status: null,
|
||||
systemStats: null,
|
||||
models: [],
|
||||
selectedModel: "",
|
||||
loadingModels: false,
|
||||
@@ -154,6 +159,14 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
refreshSystemStats: async () => {
|
||||
try {
|
||||
set({ systemStats: await getSystemStats() });
|
||||
} catch {
|
||||
set({ systemStats: null });
|
||||
}
|
||||
},
|
||||
|
||||
refreshModels: async () => {
|
||||
set({ loadingModels: true });
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user