294 lines
12 KiB
Vue
294 lines
12 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { ref } from 'vue'
|
||
|
|
|
||
|
|
interface ModelAPI {
|
||
|
|
id: number
|
||
|
|
name: string
|
||
|
|
provider: string
|
||
|
|
status: 'active' | 'inactive' | 'error'
|
||
|
|
model: string
|
||
|
|
requests: number
|
||
|
|
latency: string
|
||
|
|
createdAt: string
|
||
|
|
description: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const modelAPIs = ref<ModelAPI[]>([
|
||
|
|
{ id: 1, name: 'OpenAI Primary', provider: 'OpenAI', status: 'active', model: 'gpt-4o', requests: 1250, latency: '45ms', createdAt: '2025-04-10', description: 'Primary OpenAI API endpoint' },
|
||
|
|
{ id: 2, name: 'OpenAI Backup', provider: 'OpenAI', status: 'inactive', model: 'gpt-4o-mini', requests: 0, latency: '0ms', createdAt: '2025-04-08', description: 'Backup OpenAI API endpoint' },
|
||
|
|
{ id: 3, name: 'Google Gemini', provider: 'Google', status: 'active', model: 'gemini-2.0-flash', requests: 890, latency: '32ms', createdAt: '2025-04-05', description: 'Google Gemini API integration' },
|
||
|
|
{ id: 4, name: 'Cerebras Fast', provider: 'Cerebras', status: 'active', model: 'cerebras-sandbox', requests: 2100, latency: '12ms', createdAt: '2025-04-12', description: 'Cerebras high-speed inference' },
|
||
|
|
{ id: 5, name: 'Anthropic Claude', provider: 'Anthropic', status: 'error', model: 'claude-3-5-sonnet', requests: 450, latency: '0ms', createdAt: '2025-04-11', description: 'Anthropic Claude API' },
|
||
|
|
{ id: 6, name: 'Azure OpenAI', provider: 'Microsoft', status: 'active', model: 'gpt-4', requests: 680, latency: '55ms', createdAt: '2025-04-09', description: 'Azure-hosted OpenAI models' },
|
||
|
|
])
|
||
|
|
|
||
|
|
const editingModel = ref<ModelAPI | null>(null)
|
||
|
|
const isEditing = ref(false)
|
||
|
|
const searchQuery = ref('')
|
||
|
|
const filterStatus = ref<string>('all')
|
||
|
|
|
||
|
|
const editForm = ref({
|
||
|
|
name: '',
|
||
|
|
provider: '',
|
||
|
|
model: '',
|
||
|
|
description: '',
|
||
|
|
})
|
||
|
|
|
||
|
|
const openEdit = (model: ModelAPI) => {
|
||
|
|
editingModel.value = model
|
||
|
|
editForm.value = {
|
||
|
|
name: model.name,
|
||
|
|
provider: model.provider,
|
||
|
|
model: model.model,
|
||
|
|
description: model.description,
|
||
|
|
}
|
||
|
|
isEditing.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const saveEdit = () => {
|
||
|
|
if (editingModel.value) {
|
||
|
|
const index = modelAPIs.value.findIndex(m => m.id === editingModel.value!.id)
|
||
|
|
if (index !== -1) {
|
||
|
|
modelAPIs.value[index] = {
|
||
|
|
...modelAPIs.value[index],
|
||
|
|
...editForm.value,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
isEditing.value = false
|
||
|
|
}
|
||
|
|
|
||
|
|
const cancelEdit = () => {
|
||
|
|
isEditing.value = false
|
||
|
|
editingModel.value = null
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleStatus = (model: ModelAPI) => {
|
||
|
|
if (model.status === 'active') {
|
||
|
|
model.status = 'inactive'
|
||
|
|
} else if (model.status === 'inactive') {
|
||
|
|
model.status = 'active'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const deleteModel = (id: number) => {
|
||
|
|
modelAPIs.value = modelAPIs.value.filter(m => m.id !== id)
|
||
|
|
}
|
||
|
|
|
||
|
|
const filteredModels = () => {
|
||
|
|
return modelAPIs.value.filter(model => {
|
||
|
|
const matchSearch = model.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||
|
|
model.provider.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||
|
|
model.model.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||
|
|
const matchStatus = filterStatus.value === 'all' || model.status === filterStatus.value
|
||
|
|
return matchSearch && matchStatus
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const statusClass = (status: string) => {
|
||
|
|
switch (status) {
|
||
|
|
case 'active': return 'bg-primary-success'
|
||
|
|
case 'inactive': return 'bg-gray-500'
|
||
|
|
case 'error': return 'bg-primary-danger'
|
||
|
|
default: return 'bg-gray-500'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const providerIcon = (provider: string) => {
|
||
|
|
switch (provider) {
|
||
|
|
case 'OpenAI': return 'fa-openai'
|
||
|
|
case 'Google': return 'fa-google'
|
||
|
|
case 'Cerebras': return 'fa-microchip'
|
||
|
|
case 'Anthropic': return 'fa-robot'
|
||
|
|
case 'Microsoft': return 'fa-microsoft'
|
||
|
|
default: return 'fa-cube'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div class="p-6 min-h-screen">
|
||
|
|
<div class="flex justify-between items-center mb-6">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<i class="fa-solid fa-cube text-gray-400"></i>
|
||
|
|
<span class="font-medium">Model APIs</span>
|
||
|
|
</div>
|
||
|
|
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||
|
|
<i class="fa-solid fa-plus"></i>
|
||
|
|
New Model API
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex gap-4 mb-6">
|
||
|
|
<div class="flex-1 relative">
|
||
|
|
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||
|
|
<input
|
||
|
|
v-model="searchQuery"
|
||
|
|
type="text"
|
||
|
|
placeholder="Search model APIs..."
|
||
|
|
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
<select
|
||
|
|
v-model="filterStatus"
|
||
|
|
class="bg-dark-600 border border-dark-500 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-primary-orange"
|
||
|
|
>
|
||
|
|
<option value="all">All Status</option>
|
||
|
|
<option value="active">Active</option>
|
||
|
|
<option value="inactive">Inactive</option>
|
||
|
|
<option value="error">Error</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||
|
|
<table class="w-full">
|
||
|
|
<thead class="bg-dark-600">
|
||
|
|
<tr>
|
||
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">API Name</th>
|
||
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
|
||
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
|
||
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Requests</th>
|
||
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Latency</th>
|
||
|
|
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||
|
|
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr v-for="model in filteredModels()" :key="model.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
|
||
|
|
<td class="px-5 py-4">
|
||
|
|
<div class="font-medium">{{ model.name }}</div>
|
||
|
|
<div class="text-sm text-gray-500">{{ model.description }}</div>
|
||
|
|
</td>
|
||
|
|
<td class="px-5 py-4">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<i :class="['fa-brands', providerIcon(model.provider), 'text-lg']"></i>
|
||
|
|
<span>{{ model.provider }}</span>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td class="px-5 py-4">
|
||
|
|
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ model.model }}</span>
|
||
|
|
</td>
|
||
|
|
<td class="px-5 py-4 text-gray-300">{{ model.requests.toLocaleString() }}</td>
|
||
|
|
<td class="px-5 py-4">
|
||
|
|
<span :class="model.latency === '0ms' ? 'text-gray-500' : 'text-primary-cyan'">{{ model.latency }}</span>
|
||
|
|
</td>
|
||
|
|
<td class="px-5 py-4">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<span class="w-2 h-2 rounded-full" :class="statusClass(model.status)"></span>
|
||
|
|
<span class="capitalize text-sm">{{ model.status }}</span>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td class="px-5 py-4">
|
||
|
|
<div class="flex items-center justify-end gap-2">
|
||
|
|
<button
|
||
|
|
@click="toggleStatus(model)"
|
||
|
|
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||
|
|
:title="model.status === 'active' ? 'Deactivate' : 'Activate'"
|
||
|
|
>
|
||
|
|
<i :class="['fa-solid', model.status === 'active' ? 'fa-pause' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
@click="openEdit(model)"
|
||
|
|
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||
|
|
title="Edit"
|
||
|
|
>
|
||
|
|
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
@click="deleteModel(model.id)"
|
||
|
|
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||
|
|
title="Delete"
|
||
|
|
>
|
||
|
|
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
|
||
|
|
<div v-if="filteredModels().length === 0" class="py-12 text-center text-gray-500">
|
||
|
|
<i class="fa-solid fa-cube text-4xl mb-3"></i>
|
||
|
|
<p>No model APIs found</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Teleport to="body">
|
||
|
|
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click.self="cancelEdit">
|
||
|
|
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||
|
|
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||
|
|
<h3 class="text-lg font-semibold">Edit Model API</h3>
|
||
|
|
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
|
||
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="p-5 space-y-4">
|
||
|
|
<div>
|
||
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">API Name</label>
|
||
|
|
<input
|
||
|
|
v-model="editForm.name"
|
||
|
|
type="text"
|
||
|
|
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
|
||
|
|
<select
|
||
|
|
v-model="editForm.provider"
|
||
|
|
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||
|
|
>
|
||
|
|
<option value="OpenAI">OpenAI</option>
|
||
|
|
<option value="Google">Google</option>
|
||
|
|
<option value="Cerebras">Cerebras</option>
|
||
|
|
<option value="Anthropic">Anthropic</option>
|
||
|
|
<option value="Microsoft">Microsoft</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
|
||
|
|
<select
|
||
|
|
v-model="editForm.model"
|
||
|
|
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||
|
|
>
|
||
|
|
<option value="gpt-4o">gpt-4o</option>
|
||
|
|
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
||
|
|
<option value="gpt-4">gpt-4</option>
|
||
|
|
<option value="gemini-2.0-flash">gemini-2.0-flash</option>
|
||
|
|
<option value="cerebras-sandbox">cerebras-sandbox</option>
|
||
|
|
<option value="claude-3-5-sonnet">claude-3-5-sonnet</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||
|
|
<textarea
|
||
|
|
v-model="editForm.description"
|
||
|
|
rows="3"
|
||
|
|
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
|
||
|
|
></textarea>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||
|
|
<button
|
||
|
|
@click="cancelEdit"
|
||
|
|
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
@click="saveEdit"
|
||
|
|
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||
|
|
>
|
||
|
|
Save Changes
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Teleport>
|
||
|
|
</div>
|
||
|
|
</template>
|