Add SystemInfoModal component and integrate system info fetching in App

This commit is contained in:
2025-07-24 19:36:26 +02:00
parent 40e3a8bb7a
commit cf8d581275
4 changed files with 265 additions and 33 deletions

View File

@@ -1,48 +1,61 @@
import { useState } from 'react' import { useState } from "react";
import Header from '@/components/Header' import Header from "@/components/Header";
import InstanceList from '@/components/InstanceList' import InstanceList from "@/components/InstanceList";
import InstanceModal from '@/components/InstanceModal' import InstanceModal from "@/components/InstanceModal";
import { CreateInstanceOptions, Instance } from '@/types/instance' import { CreateInstanceOptions, Instance } from "@/types/instance";
import { useInstances } from '@/contexts/InstancesContext' import { useInstances } from "@/contexts/InstancesContext";
import SystemInfoModal from "./components/SystemInfoModal";
function App() { function App() {
const [isModalOpen, setIsModalOpen] = useState(false) const [isInstanceModalOpen, setIsInstanceModalOpen] = useState(false);
const [editingInstance, setEditingInstance] = useState<Instance | undefined>(undefined) const [isSystemInfoModalOpen, setIsSystemInfoModalOpen] = useState(false);
const { createInstance, updateInstance } = useInstances() const [editingInstance, setEditingInstance] = useState<Instance | undefined>(
undefined
);
const { createInstance, updateInstance } = useInstances();
const handleCreateInstance = () => { const handleCreateInstance = () => {
setEditingInstance(undefined) setEditingInstance(undefined);
setIsModalOpen(true) setIsInstanceModalOpen(true);
} };
const handleEditInstance = (instance: Instance) => { const handleEditInstance = (instance: Instance) => {
setEditingInstance(instance) setEditingInstance(instance);
setIsModalOpen(true) setIsInstanceModalOpen(true);
} };
const handleSaveInstance = (name: string, options: CreateInstanceOptions) => { const handleSaveInstance = (name: string, options: CreateInstanceOptions) => {
if (editingInstance) { if (editingInstance) {
updateInstance(editingInstance.name, options) updateInstance(editingInstance.name, options);
} else { } else {
createInstance(name, options) createInstance(name, options);
} }
} };
const handleShowSystemInfo = () => {
setIsSystemInfoModalOpen(true);
};
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<Header onCreateInstance={handleCreateInstance} /> <Header onCreateInstance={handleCreateInstance} onShowSystemInfo={handleShowSystemInfo} />
<main className="container mx-auto max-w-4xl px-4 py-8"> <main className="container mx-auto max-w-4xl px-4 py-8">
<InstanceList editInstance={handleEditInstance} /> <InstanceList editInstance={handleEditInstance} />
</main> </main>
<InstanceModal <InstanceModal
open={isModalOpen} open={isInstanceModalOpen}
onOpenChange={setIsModalOpen} onOpenChange={setIsInstanceModalOpen}
onSave={handleSaveInstance} onSave={handleSaveInstance}
instance={editingInstance} instance={editingInstance}
/> />
<SystemInfoModal
open={isSystemInfoModalOpen}
onOpenChange={setIsSystemInfoModalOpen}
/>
</div> </div>
) );
} }
export default App export default App;

View File

@@ -1,10 +1,12 @@
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { HelpCircle } from "lucide-react";
interface HeaderProps { interface HeaderProps {
onCreateInstance: () => void onCreateInstance: () => void;
onShowSystemInfo: () => void;
} }
function Header({ onCreateInstance }: HeaderProps) { function Header({ onCreateInstance, onShowSystemInfo }: HeaderProps) {
return ( return (
<header className="bg-white border-b border-gray-200"> <header className="bg-white border-b border-gray-200">
<div className="container mx-auto max-w-4xl px-4 py-4"> <div className="container mx-auto max-w-4xl px-4 py-4">
@@ -13,13 +15,22 @@ function Header({ onCreateInstance }: HeaderProps) {
LlamaCtl Dashboard LlamaCtl Dashboard
</h1> </h1>
<Button onClick={onCreateInstance}> <div className="flex items-center gap-2">
Create Instance <Button onClick={onCreateInstance}>Create Instance</Button>
</Button>
<Button
variant="outline"
size="icon"
onClick={onShowSystemInfo}
title="System Info"
>
<HelpCircle className="h-4 w-4" />
</Button>
</div>
</div> </div>
</div> </div>
</header> </header>
) );
} }
export default Header export default Header;

View File

@@ -0,0 +1,184 @@
// ui/src/components/SystemInfoModal.tsx
import React, { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
RefreshCw,
AlertCircle,
Loader2,
ChevronDown,
ChevronRight,
Monitor,
HelpCircle
} from 'lucide-react'
import { serverApi } from '@/lib/api'
interface SystemInfoModalProps {
open: boolean
onOpenChange: (open: boolean) => void
}
interface SystemInfo {
version: string
devices: string
help: string
}
const SystemInfoModal: React.FC<SystemInfoModalProps> = ({
open,
onOpenChange
}) => {
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [showHelp, setShowHelp] = useState(false)
// Fetch system info
const fetchSystemInfo = async () => {
setLoading(true)
setError(null)
try {
const [version, devices, help] = await Promise.all([
serverApi.getVersion(),
serverApi.getDevices(),
serverApi.getHelp()
])
setSystemInfo({ version, devices, help })
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch system info')
} finally {
setLoading(false)
}
}
// Load data when modal opens
useEffect(() => {
if (open) {
fetchSystemInfo()
}
}, [open])
return (
<Dialog open={open} onOpenChange={onOpenChange} >
<DialogContent className="sm:max-w-4xl max-w-[calc(100%-2rem)] max-h-[80vh] flex flex-col">
<DialogHeader>
<div className="flex items-center justify-between">
<div>
<DialogTitle className="flex items-center gap-2">
<Monitor className="h-5 w-5" />
System Information
</DialogTitle>
<DialogDescription>
Llama.cpp server environment and capabilities
</DialogDescription>
</div>
<Button
variant="outline"
size="sm"
onClick={fetchSystemInfo}
disabled={loading}
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
{loading && !systemInfo ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-400">Loading system information...</span>
</div>
) : error ? (
<div className="flex items-center gap-2 p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
<AlertCircle className="h-4 w-4 text-destructive" />
<span className="text-sm text-destructive">{error}</span>
</div>
) : systemInfo ? (
<div className="space-y-6">
{/* Version Section */}
<div className="space-y-3">
<h3 className="font-semibold">Version</h3>
<div className="bg-gray-900 rounded-lg p-4">
<div className="mb-2">
<span className="text-sm text-gray-400">$ llama-server --version</span>
</div>
<pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">
{systemInfo.version}
</pre>
</div>
</div>
{/* Devices Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="font-semibold">Available Devices</h3>
</div>
<div className="bg-gray-900 rounded-lg p-4">
<div className="mb-2">
<span className="text-sm text-gray-400">$ llama-server --list-devices</span>
</div>
<pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono">
{systemInfo.devices}
</pre>
</div>
</div>
{/* Help Section */}
<div className="space-y-3">
<Button
variant="ghost"
onClick={() => setShowHelp(!showHelp)}
className="flex items-center gap-2 p-0 h-auto font-semibold"
>
{showHelp ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<HelpCircle className="h-4 w-4" />
Command Line Options
</Button>
{showHelp && (
<div className="bg-gray-900 rounded-lg p-4">
<div className="mb-2">
<span className="text-sm text-gray-400">$ llama-server --help</span>
</div>
<pre className="text-sm text-gray-300 whitespace-pre-wrap font-mono max-h-64 overflow-y-auto">
{systemInfo.help}
</pre>
</div>
)}
</div>
</div>
) : null}
</div>
<DialogFooter>
<Button onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default SystemInfoModal

View File

@@ -68,6 +68,30 @@ async function apiCall<T>(
} }
} }
// Server API functions
export const serverApi = {
// GET /server/help
getHelp: async (): Promise<string> => {
const response = await fetch(`${API_BASE}/server/help`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
// GET /server/version
getVersion: async (): Promise<string> => {
const response = await fetch(`${API_BASE}/server/version`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
// GET /server/devices
getDevices: async (): Promise<string> => {
const response = await fetch(`${API_BASE}/server/devices`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
}
// Instance API functions // Instance API functions
export const instancesApi = { export const instancesApi = {
// GET /instances // GET /instances