446 lines
16 KiB
TypeScript
446 lines
16 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Alert, AlertDescription } from "@/components/ui/alert"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Edit2, Save, X, Loader2, RefreshCw } from "lucide-react"
|
|
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
|
|
|
|
// Atualizar a interface ModelPrice para refletir a nova estrutura da API:
|
|
interface ModelPrice {
|
|
_id: {
|
|
$oid: string
|
|
}
|
|
provider: string
|
|
product: string
|
|
type: string
|
|
billingBy: string
|
|
billingUnit: number
|
|
currency: string
|
|
price: string
|
|
clientPrice?: string // Campo opcional que pode não vir da API
|
|
createdAt: {
|
|
$date: string
|
|
}
|
|
updatedAt: {
|
|
$date: string
|
|
}
|
|
__v: number
|
|
}
|
|
|
|
// Interface para a resposta da API:
|
|
interface ApiResponse {
|
|
success: boolean
|
|
data: ModelPrice[]
|
|
}
|
|
|
|
export default function ModelPricesTable() {
|
|
const [data, setData] = useState<ModelPrice[]>([])
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState("")
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
// Atualizar editValues para incluir todos os campos editáveis:
|
|
const [editValues, setEditValues] = useState<{
|
|
product: string
|
|
provider: string
|
|
type: string
|
|
billingBy: string
|
|
billingUnit: string
|
|
currency: string
|
|
clientPrice: string
|
|
price: string
|
|
}>({
|
|
product: "",
|
|
provider: "",
|
|
type: "",
|
|
billingBy: "",
|
|
billingUnit: "",
|
|
currency: "",
|
|
clientPrice: "",
|
|
price: "",
|
|
})
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
|
|
// Filtros
|
|
const [typeFilter, setTypeFilter] = useState("stt")
|
|
const [providerFilter, setProviderFilter] = useState("")
|
|
|
|
const getAuthHeaders = () => {
|
|
const token = localStorage.getItem("access_token")
|
|
return {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
}
|
|
}
|
|
|
|
const fetchData = async () => {
|
|
setIsLoading(true)
|
|
setError("")
|
|
|
|
try {
|
|
const params = new URLSearchParams()
|
|
if (typeFilter) params.append("type", typeFilter)
|
|
if (providerFilter) params.append("provider", providerFilter)
|
|
|
|
const response = await fetch(`${API_BASE_URL}/usage/model/prices?${params}`, {
|
|
headers: getAuthHeaders(),
|
|
})
|
|
|
|
if (response.ok) {
|
|
// Atualizar o processamento da resposta para a nova estrutura:
|
|
const result: ApiResponse = await response.json()
|
|
if (result.success && Array.isArray(result.data)) {
|
|
setData(result.data)
|
|
} else {
|
|
setData([])
|
|
}
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || "Erro ao buscar dados")
|
|
}
|
|
} catch (err) {
|
|
console.log("====> Erro de conexão com o servidor: ", err)
|
|
setError("Erro de conexão com o servidor")
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
// Atualizar a função startEdit para trabalhar com todos os campos:
|
|
const startEdit = (item: ModelPrice) => {
|
|
const id = item._id.$oid
|
|
setEditingId(id)
|
|
setEditValues({
|
|
product: item.product,
|
|
provider: item.provider,
|
|
type: item.type,
|
|
billingBy: item.billingBy,
|
|
billingUnit: item.billingUnit.toString(),
|
|
currency: item.currency,
|
|
clientPrice: item.clientPrice || "",
|
|
price: item.price,
|
|
})
|
|
}
|
|
|
|
const cancelEdit = () => {
|
|
setEditingId(null)
|
|
setEditValues({
|
|
product: "",
|
|
provider: "",
|
|
type: "",
|
|
billingBy: "",
|
|
billingUnit: "",
|
|
currency: "",
|
|
clientPrice: "",
|
|
price: "",
|
|
})
|
|
}
|
|
|
|
// Atualizar a função saveEdit para enviar todos os campos:
|
|
const saveEdit = async (id: string) => {
|
|
setIsSaving(true)
|
|
setError("")
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/usage/model/prices/${id}`, {
|
|
method: "PATCH",
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
product: editValues.product,
|
|
provider: editValues.provider,
|
|
type: editValues.type,
|
|
billingBy: editValues.billingBy,
|
|
billingUnit: Number.parseInt(editValues.billingUnit),
|
|
currency: editValues.currency,
|
|
clientPrice: editValues.clientPrice,
|
|
price: editValues.price,
|
|
}),
|
|
})
|
|
|
|
if (response.ok) {
|
|
await fetchData() // Recarrega os dados
|
|
setEditingId(null)
|
|
setEditValues({
|
|
product: "",
|
|
provider: "",
|
|
type: "",
|
|
billingBy: "",
|
|
billingUnit: "",
|
|
currency: "",
|
|
clientPrice: "",
|
|
price: "",
|
|
})
|
|
} else {
|
|
const errorData = await response.json()
|
|
setError(errorData.message || "Erro ao salvar alterações")
|
|
}
|
|
} catch (err) {
|
|
console.log("====> Erro de conexão com o servidor: ", err)
|
|
setError("Erro de conexão com o servidor")
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [])
|
|
|
|
// Opções para os selects
|
|
const typeOptions = ["stt", "tts", "llm", "embedding", "vision"]
|
|
const providerOptions = ["openai", "aws", "google", "anthropic", "mistral", "azure"]
|
|
const currencyOptions = ["dollar", "real", "euro"]
|
|
const billingByOptions = ["second", "minute", "character", "token", "image", "request"]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Filtros */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Filtros</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="typeFilter">Tipo</Label>
|
|
<Input
|
|
id="typeFilter"
|
|
value={typeFilter}
|
|
onChange={(e) => setTypeFilter(e.target.value)}
|
|
placeholder="stt,tts"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="providerFilter">Provedor</Label>
|
|
<Input
|
|
id="providerFilter"
|
|
value={providerFilter}
|
|
onChange={(e) => setProviderFilter(e.target.value)}
|
|
placeholder="openai,aws"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label> </Label>
|
|
<Button onClick={fetchData} disabled={isLoading} className="w-full">
|
|
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
|
Atualizar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Tabela de Preços */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Preços dos Modelos ({data.length} modelos)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{data.length > 0 ? (
|
|
<div className="overflow-x-auto">
|
|
{/* Atualizar a tabela para exibir os novos campos: */}
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Produto</TableHead>
|
|
<TableHead>Provedor</TableHead>
|
|
<TableHead>Tipo</TableHead>
|
|
<TableHead>Cobrança Por</TableHead>
|
|
<TableHead>Unidade</TableHead>
|
|
<TableHead>Moeda</TableHead>
|
|
<TableHead>Preço</TableHead>
|
|
<TableHead>Preço Cliente</TableHead>
|
|
<TableHead>Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((item) => {
|
|
const itemId = item._id.$oid
|
|
const isEditing = editingId === itemId
|
|
|
|
return (
|
|
<TableRow key={itemId}>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Input
|
|
value={editValues.product}
|
|
onChange={(e) => setEditValues((prev) => ({ ...prev, product: e.target.value }))}
|
|
className="w-32"
|
|
/>
|
|
) : (
|
|
item.product || "-"
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Select
|
|
value={editValues.provider}
|
|
onValueChange={(value) => setEditValues((prev) => ({ ...prev, provider: value }))}
|
|
>
|
|
<SelectTrigger className="w-28">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{providerOptions.map((option) => (
|
|
<SelectItem key={option} value={option}>
|
|
{option}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<Badge variant="secondary">{item.provider}</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Select
|
|
value={editValues.type}
|
|
onValueChange={(value) => setEditValues((prev) => ({ ...prev, type: value }))}
|
|
>
|
|
<SelectTrigger className="w-28">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{typeOptions.map((option) => (
|
|
<SelectItem key={option} value={option}>
|
|
{option}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<Badge variant="outline">{item.type}</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Select
|
|
value={editValues.billingBy}
|
|
onValueChange={(value) => setEditValues((prev) => ({ ...prev, billingBy: value }))}
|
|
>
|
|
<SelectTrigger className="w-28">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{billingByOptions.map((option) => (
|
|
<SelectItem key={option} value={option}>
|
|
{option}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
item.billingBy
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Input
|
|
type="number"
|
|
value={editValues.billingUnit}
|
|
onChange={(e) => setEditValues((prev) => ({ ...prev, billingUnit: e.target.value }))}
|
|
className="w-20"
|
|
/>
|
|
) : (
|
|
item.billingUnit
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Select
|
|
value={editValues.currency}
|
|
onValueChange={(value) => setEditValues((prev) => ({ ...prev, currency: value }))}
|
|
>
|
|
<SelectTrigger className="w-28">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{currencyOptions.map((option) => (
|
|
<SelectItem key={option} value={option}>
|
|
{option}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<Badge variant="outline">{item.currency}</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Input
|
|
type="number"
|
|
step="0.0001"
|
|
value={editValues.price}
|
|
onChange={(e) => setEditValues((prev) => ({ ...prev, price: e.target.value }))}
|
|
className="w-24"
|
|
/>
|
|
) : (
|
|
`$${item.price}`
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<Input
|
|
type="number"
|
|
step="0.0001"
|
|
value={editValues.clientPrice}
|
|
onChange={(e) => setEditValues((prev) => ({ ...prev, clientPrice: e.target.value }))}
|
|
className="w-24"
|
|
placeholder="0.000"
|
|
/>
|
|
) : item.clientPrice ? (
|
|
`$${item.clientPrice}`
|
|
) : (
|
|
"-"
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isEditing ? (
|
|
<div className="flex gap-2">
|
|
<Button size="sm" onClick={() => saveEdit(itemId)} disabled={isSaving}>
|
|
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={cancelEdit} disabled={isSaving}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<Button size="sm" variant="outline" onClick={() => startEdit(item)}>
|
|
<Edit2 className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-500">
|
|
{isLoading ? "Carregando..." : "Nenhum modelo encontrado."}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|