mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-11-07 17:44:22 +00:00
Refactor project structure
This commit is contained in:
146
webui/src/lib/api.ts
Normal file
146
webui/src/lib/api.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { CreateInstanceOptions, Instance } from "@/types/instance"
|
||||
|
||||
const API_BASE = '/api/v1'
|
||||
|
||||
// Configuration for API calls
|
||||
// interface ApiConfig {
|
||||
// apiKey?: string
|
||||
// }
|
||||
|
||||
// Global config - can be updated when auth is added
|
||||
// let apiConfig: ApiConfig = {}
|
||||
|
||||
// export const setApiConfig = (config: ApiConfig) => {
|
||||
// apiConfig = config
|
||||
// }
|
||||
|
||||
// Base API call function with error handling
|
||||
async function apiCall<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${API_BASE}${endpoint}`
|
||||
|
||||
// Prepare headers
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
// Add API key - not supported yet
|
||||
// if (apiConfig.apiKey) {
|
||||
// headers['Authorization'] = `Bearer ${apiConfig.apiKey}`
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Try to get error message from response
|
||||
let errorMessage = `HTTP ${response.status}`
|
||||
try {
|
||||
const errorText = await response.text()
|
||||
if (errorText) {
|
||||
errorMessage += `: ${errorText}`
|
||||
}
|
||||
} catch {
|
||||
// If we can't read the error, just use status
|
||||
}
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Handle empty responses (like DELETE)
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Network error occurred')
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
export const instancesApi = {
|
||||
// GET /instances
|
||||
list: () => apiCall<Instance[]>('/instances'),
|
||||
|
||||
// GET /instances/{name}
|
||||
get: (name: string) => apiCall<Instance>(`/instances/${name}`),
|
||||
|
||||
// POST /instances/{name}
|
||||
create: (name: string, options: CreateInstanceOptions) =>
|
||||
apiCall<Instance>(`/instances/${name}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options),
|
||||
}),
|
||||
|
||||
// PUT /instances/{name}
|
||||
update: (name: string, options: CreateInstanceOptions) =>
|
||||
apiCall<Instance>(`/instances/${name}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(options),
|
||||
}),
|
||||
|
||||
// DELETE /instances/{name}
|
||||
delete: (name: string) =>
|
||||
apiCall<void>(`/instances/${name}`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
|
||||
// POST /instances/{name}/start
|
||||
start: (name: string) =>
|
||||
apiCall<Instance>(`/instances/${name}/start`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// POST /instances/{name}/stop
|
||||
stop: (name: string) =>
|
||||
apiCall<Instance>(`/instances/${name}/stop`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// POST /instances/{name}/restart
|
||||
restart: (name: string) =>
|
||||
apiCall<Instance>(`/instances/${name}/restart`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// GET /instances/{name}/logs
|
||||
getLogs: (name: string, lines?: number) => {
|
||||
const params = lines ? `?lines=${lines}` : ''
|
||||
return apiCall<string>(`/instances/${name}/logs${params}`)
|
||||
},
|
||||
}
|
||||
115
webui/src/lib/healthService.ts
Normal file
115
webui/src/lib/healthService.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { HealthStatus } from '@/types/instance'
|
||||
|
||||
type HealthCallback = (health: HealthStatus) => void
|
||||
|
||||
class HealthService {
|
||||
private intervals: Map<string, NodeJS.Timeout> = new Map()
|
||||
private callbacks: Map<string, Set<HealthCallback>> = new Map()
|
||||
|
||||
async checkHealth(instanceName: string): Promise<HealthStatus> {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/instances/${instanceName}/proxy/health`)
|
||||
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'ok',
|
||||
lastChecked: new Date()
|
||||
}
|
||||
} else if (response.status === 503) {
|
||||
const data = await response.json()
|
||||
return {
|
||||
status: 'loading',
|
||||
message: data.error.message,
|
||||
lastChecked: new Date()
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `HTTP ${response.status}`,
|
||||
lastChecked: new Date()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Network error',
|
||||
lastChecked: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(instanceName: string, callback: HealthCallback): () => void {
|
||||
if (!this.callbacks.has(instanceName)) {
|
||||
this.callbacks.set(instanceName, new Set())
|
||||
}
|
||||
|
||||
this.callbacks.get(instanceName)!.add(callback)
|
||||
|
||||
// Start health checking if this is the first subscriber
|
||||
if (this.callbacks.get(instanceName)!.size === 1) {
|
||||
this.startHealthCheck(instanceName)
|
||||
}
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const callbacks = this.callbacks.get(instanceName)
|
||||
if (callbacks) {
|
||||
callbacks.delete(callback)
|
||||
|
||||
// Stop health checking if no more subscribers
|
||||
if (callbacks.size === 0) {
|
||||
this.stopHealthCheck(instanceName)
|
||||
this.callbacks.delete(instanceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startHealthCheck(instanceName: string): void {
|
||||
if (this.intervals.has(instanceName)) {
|
||||
return // Already checking
|
||||
}
|
||||
|
||||
// Initial check with delay
|
||||
setTimeout(async () => {
|
||||
const health = await this.checkHealth(instanceName)
|
||||
this.notifyCallbacks(instanceName, health)
|
||||
|
||||
// Start periodic checks
|
||||
const interval = setInterval(async () => {
|
||||
const health = await this.checkHealth(instanceName)
|
||||
this.notifyCallbacks(instanceName, health)
|
||||
}, 60000)
|
||||
|
||||
this.intervals.set(instanceName, interval)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
private stopHealthCheck(instanceName: string): void {
|
||||
const interval = this.intervals.get(instanceName)
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
this.intervals.delete(instanceName)
|
||||
}
|
||||
}
|
||||
|
||||
private notifyCallbacks(instanceName: string, health: HealthStatus): void {
|
||||
const callbacks = this.callbacks.get(instanceName)
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => callback(health))
|
||||
}
|
||||
}
|
||||
|
||||
stopAll(): void {
|
||||
this.intervals.forEach(interval => clearInterval(interval))
|
||||
this.intervals.clear()
|
||||
this.callbacks.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export const healthService = new HealthService()
|
||||
|
||||
// Export the individual checkHealth function as well
|
||||
export async function checkHealth(instanceName: string): Promise<HealthStatus> {
|
||||
return healthService.checkHealth(instanceName)
|
||||
}
|
||||
6
webui/src/lib/utils.ts
Normal file
6
webui/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
59
webui/src/lib/zodFormUtils.ts
Normal file
59
webui/src/lib/zodFormUtils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { CreateInstanceOptions, getAllFieldKeys } from '@/schemas/instanceOptions'
|
||||
|
||||
// Only define the basic fields we want to show by default
|
||||
export const basicFieldsConfig: Record<string, {
|
||||
label: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
}> = {
|
||||
auto_restart: {
|
||||
label: 'Auto Restart',
|
||||
description: 'Automatically restart the instance on failure'
|
||||
},
|
||||
max_restarts: {
|
||||
label: 'Max Restarts',
|
||||
placeholder: '3',
|
||||
description: 'Maximum number of restart attempts (0 = unlimited)'
|
||||
},
|
||||
restart_delay: {
|
||||
label: 'Restart Delay (seconds)',
|
||||
placeholder: '5',
|
||||
description: 'Delay in seconds before attempting restart'
|
||||
},
|
||||
model: {
|
||||
label: 'Model Path',
|
||||
placeholder: '/path/to/model.gguf',
|
||||
description: 'Path to the model file'
|
||||
},
|
||||
hf_repo: {
|
||||
label: 'Hugging Face Repository',
|
||||
placeholder: 'microsoft/DialoGPT-medium',
|
||||
description: 'Hugging Face model repository'
|
||||
},
|
||||
hf_file: {
|
||||
label: 'Hugging Face File',
|
||||
placeholder: 'model.gguf',
|
||||
description: 'Specific file in the repository'
|
||||
},
|
||||
gpu_layers: {
|
||||
label: 'GPU Layers',
|
||||
placeholder: '0',
|
||||
description: 'Number of layers to offload to GPU'
|
||||
}
|
||||
}
|
||||
|
||||
export function isBasicField(key: keyof CreateInstanceOptions): boolean {
|
||||
return key in basicFieldsConfig
|
||||
}
|
||||
|
||||
export function getBasicFields(): (keyof CreateInstanceOptions)[] {
|
||||
return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[]
|
||||
}
|
||||
|
||||
export function getAdvancedFields(): (keyof CreateInstanceOptions)[] {
|
||||
return getAllFieldKeys().filter(key => !isBasicField(key))
|
||||
}
|
||||
|
||||
// Re-export the Zod-based functions
|
||||
export { getFieldType } from '@/schemas/instanceOptions'
|
||||
Reference in New Issue
Block a user