Update UI with new Activity event stream from #195
- use new metrics data instead of log parsing - auto-start events connection to server, improves responsiveness - remove unnecessary libraries and code
This commit is contained in:
@@ -44,16 +44,16 @@ interface APIEventEnvelope {
|
|||||||
const APIContext = createContext<APIProviderType | undefined>(undefined);
|
const APIContext = createContext<APIProviderType | undefined>(undefined);
|
||||||
type APIProviderProps = {
|
type APIProviderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
autoStartAPIEvents?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function APIProvider({ children }: APIProviderProps) {
|
export function APIProvider({ children, autoStartAPIEvents = true }: APIProviderProps) {
|
||||||
const [proxyLogs, setProxyLogs] = useState("");
|
const [proxyLogs, setProxyLogs] = useState("");
|
||||||
const [upstreamLogs, setUpstreamLogs] = useState("");
|
const [upstreamLogs, setUpstreamLogs] = useState("");
|
||||||
const [metrics, setMetrics] = useState<Metrics[]>([]);
|
const [metrics, setMetrics] = useState<Metrics[]>([]);
|
||||||
const apiEventSource = useRef<EventSource | null>(null);
|
const apiEventSource = useRef<EventSource | null>(null);
|
||||||
|
|
||||||
const [models, setModels] = useState<Model[]>([]);
|
const [models, setModels] = useState<Model[]>([]);
|
||||||
const modelStatusEventSource = useRef<EventSource | null>(null);
|
|
||||||
|
|
||||||
const appendLog = useCallback((newData: string, setter: React.Dispatch<React.SetStateAction<string>>) => {
|
const appendLog = useCallback((newData: string, setter: React.Dispatch<React.SetStateAction<string>>) => {
|
||||||
setter((prev) => {
|
setter((prev) => {
|
||||||
@@ -66,6 +66,7 @@ export function APIProvider({ children }: APIProviderProps) {
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
apiEventSource.current?.close();
|
apiEventSource.current?.close();
|
||||||
apiEventSource.current = null;
|
apiEventSource.current = null;
|
||||||
|
setMetrics([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ export function APIProvider({ children }: APIProviderProps) {
|
|||||||
case "metrics":
|
case "metrics":
|
||||||
{
|
{
|
||||||
const newMetric = JSON.parse(message.data) as Metrics;
|
const newMetric = JSON.parse(message.data) as Metrics;
|
||||||
setMetrics(prevMetrics => {
|
setMetrics((prevMetrics) => {
|
||||||
return [newMetric, ...prevMetrics];
|
return [newMetric, ...prevMetrics];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,10 +126,14 @@ export function APIProvider({ children }: APIProviderProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (autoStartAPIEvents) {
|
||||||
|
enableAPIEvents(true);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
modelStatusEventSource.current?.close();
|
enableAPIEvents(false);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [enableAPIEvents, autoStartAPIEvents]);
|
||||||
|
|
||||||
const listModels = useCallback(async (): Promise<Model[]> => {
|
const listModels = useCallback(async (): Promise<Model[]> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
export function processEvalTimes(text: string) {
|
|
||||||
const lines = text.match(/^ *eval time.*$/gm) || [];
|
|
||||||
|
|
||||||
let totalTokens = 0;
|
|
||||||
let totalTime = 0;
|
|
||||||
|
|
||||||
lines.forEach((line) => {
|
|
||||||
const tokensMatch = line.match(/\/\s*(\d+)\s*tokens/);
|
|
||||||
const timeMatch = line.match(/=\s*(\d+\.\d+)\s*ms/);
|
|
||||||
|
|
||||||
if (tokensMatch) totalTokens += parseFloat(tokensMatch[1]);
|
|
||||||
if (timeMatch) totalTime += parseFloat(timeMatch[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const avgTokensPerSecond = totalTime > 0 ? totalTokens / (totalTime / 1000) : 0;
|
|
||||||
|
|
||||||
return [lines.length, totalTokens, Math.round(avgTokensPerSecond * 100) / 100];
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { useAPI } from '../contexts/APIProvider';
|
import { useAPI } from "../contexts/APIProvider";
|
||||||
|
|
||||||
const ActivityPage = () => {
|
const ActivityPage = () => {
|
||||||
const { metrics, enableAPIEvents } = useAPI();
|
const { metrics } = useAPI();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
enableAPIEvents(true);
|
|
||||||
return () => {
|
|
||||||
enableAPIEvents(false);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metrics.length > 0) {
|
if (metrics.length > 0) {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -23,11 +16,11 @@ const ActivityPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatSpeed = (speed: number) => {
|
const formatSpeed = (speed: number) => {
|
||||||
return speed.toFixed(2) + ' t/s';
|
return speed.toFixed(2) + " t/s";
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDuration = (ms: number) => {
|
const formatDuration = (ms: number) => {
|
||||||
return (ms / 1000).toFixed(2) + 's';
|
return (ms / 1000).toFixed(2) + "s";
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -54,47 +47,23 @@ const ActivityPage = () => {
|
|||||||
<table className="min-w-full divide-y">
|
<table className="min-w-full divide-y">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Timestamp</th>
|
||||||
Timestamp
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Model</th>
|
||||||
</th>
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Input Tokens</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Output Tokens</th>
|
||||||
Model
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Processing Speed</th>
|
||||||
</th>
|
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Duration</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
|
|
||||||
Input Tokens
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
|
|
||||||
Output Tokens
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
|
|
||||||
Processing Speed
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
|
|
||||||
Duration
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y">
|
<tbody className="divide-y">
|
||||||
{metrics.map((metric, index) => (
|
{metrics.map((metric, index) => (
|
||||||
<tr key={`${metric.id}-${index}`}>
|
<tr key={`${metric.id}-${index}`}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatTimestamp(metric.timestamp)}</td>
|
||||||
{formatTimestamp(metric.timestamp)}
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.model}</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.input_tokens.toLocaleString()}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.output_tokens.toLocaleString()}</td>
|
||||||
{metric.model}
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatSpeed(metric.tokens_per_second)}</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatDuration(metric.duration_ms)}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
{metric.input_tokens.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
{metric.output_tokens.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
{formatSpeed(metric.tokens_per_second)}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
|
||||||
{formatDuration(metric.duration_ms)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import { useAPI } from "../contexts/APIProvider";
|
|||||||
import { usePersistentState } from "../hooks/usePersistentState";
|
import { usePersistentState } from "../hooks/usePersistentState";
|
||||||
|
|
||||||
const LogViewer = () => {
|
const LogViewer = () => {
|
||||||
const { proxyLogs, upstreamLogs, enableAPIEvents } = useAPI();
|
const { proxyLogs, upstreamLogs } = useAPI();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
enableAPIEvents(true);
|
|
||||||
return () => {
|
|
||||||
enableAPIEvents(false);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5" style={{ height: "calc(100vh - 125px)" }}>
|
<div className="flex flex-col gap-5" style={{ height: "calc(100vh - 125px)" }}>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { useAPI } from "../contexts/APIProvider";
|
import { useAPI } from "../contexts/APIProvider";
|
||||||
import { LogPanel } from "./LogViewer";
|
import { LogPanel } from "./LogViewer";
|
||||||
import { processEvalTimes } from "../lib/Utils";
|
|
||||||
import { usePersistentState } from "../hooks/usePersistentState";
|
import { usePersistentState } from "../hooks/usePersistentState";
|
||||||
|
|
||||||
export default function ModelsPage() {
|
export default function ModelsPage() {
|
||||||
const { models, unloadAllModels, loadModel, upstreamLogs, enableAPIEvents } = useAPI();
|
const { models, unloadAllModels, loadModel, upstreamLogs, metrics } = useAPI();
|
||||||
const [isUnloading, setIsUnloading] = useState(false);
|
const [isUnloading, setIsUnloading] = useState(false);
|
||||||
const [showUnlisted, setShowUnlisted] = usePersistentState("showUnlisted", true);
|
const [showUnlisted, setShowUnlisted] = usePersistentState("showUnlisted", true);
|
||||||
|
|
||||||
@@ -13,13 +12,6 @@ export default function ModelsPage() {
|
|||||||
return models.filter((model) => showUnlisted || !model.unlisted);
|
return models.filter((model) => showUnlisted || !model.unlisted);
|
||||||
}, [models, showUnlisted]);
|
}, [models, showUnlisted]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
enableAPIEvents(true);
|
|
||||||
return () => {
|
|
||||||
enableAPIEvents(false);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUnloadAllModels = useCallback(async () => {
|
const handleUnloadAllModels = useCallback(async () => {
|
||||||
setIsUnloading(true);
|
setIsUnloading(true);
|
||||||
try {
|
try {
|
||||||
@@ -34,9 +26,12 @@ export default function ModelsPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [totalLines, totalTokens, avgTokensPerSecond] = useMemo(() => {
|
const [totalRequests, totalTokens, avgTokensPerSecond] = useMemo(() => {
|
||||||
return processEvalTimes(upstreamLogs);
|
const totalTokens = metrics.reduce((sum, m) => sum + m.input_tokens + m.output_tokens, 0);
|
||||||
}, [upstreamLogs]);
|
const totalSeconds = metrics.reduce((sum, m) => sum + m.duration_ms / 1000, 0);
|
||||||
|
const avgTokensPerSecond = totalSeconds > 0 ? totalTokens / totalSeconds : 0;
|
||||||
|
return [metrics.length, totalTokens, avgTokensPerSecond.toFixed(2)];
|
||||||
|
}, [metrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -96,14 +91,13 @@ export default function ModelsPage() {
|
|||||||
|
|
||||||
{/* Right Column */}
|
{/* Right Column */}
|
||||||
<div className="w-full md:w-1/2 flex flex-col" style={{ height: "calc(100vh - 125px)" }}>
|
<div className="w-full md:w-1/2 flex flex-col" style={{ height: "calc(100vh - 125px)" }}>
|
||||||
<div className="card mb-4 min-h-[250px]">
|
<div className="card mb-4 min-h-[225px]">
|
||||||
<h2>Log Stats</h2>
|
<h2>Chat Activity</h2>
|
||||||
<p className="italic my-2">note: eval logs from llama-server</p>
|
|
||||||
<table className="w-full border border-gray-200">
|
<table className="w-full border border-gray-200">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
<td className="py-2 px-4 font-medium border-r border-gray-200">Requests</td>
|
<td className="py-2 px-4 font-medium border-r border-gray-200">Requests</td>
|
||||||
<td className="py-2 px-4 text-right">{totalLines}</td>
|
<td className="py-2 px-4 text-right">{totalRequests}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
<td className="py-2 px-4 font-medium border-r border-gray-200">Total Tokens Generated</td>
|
<td className="py-2 px-4 font-medium border-r border-gray-200">Total Tokens Generated</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user