| "use client";
|
|
|
| import { useState, useEffect } from "react";
|
| import { useTranslations } from "next-intl";
|
| import { getDefaultPricing, formatCost } from "@/shared/constants/pricing";
|
|
|
| export default function PricingModal({ isOpen, onClose, onSave }) {
|
| const t = useTranslations("pricingModal");
|
| const [pricingData, setPricingData] = useState({});
|
| const [loading, setLoading] = useState(true);
|
| const [saving, setSaving] = useState(false);
|
|
|
| useEffect(() => {
|
| if (isOpen) {
|
| loadPricing();
|
| }
|
| }, [isOpen]);
|
|
|
| const loadPricing = async () => {
|
| setLoading(true);
|
| try {
|
| const response = await fetch("/api/pricing");
|
| if (response.ok) {
|
| const data = await response.json();
|
| setPricingData(data);
|
| } else {
|
|
|
| const defaults = getDefaultPricing();
|
| setPricingData(defaults);
|
| }
|
| } catch (error) {
|
| console.error("Failed to load pricing:", error);
|
| const defaults = getDefaultPricing();
|
| setPricingData(defaults);
|
| } finally {
|
| setLoading(false);
|
| }
|
| };
|
|
|
| const handlePricingChange = (provider, model, field, value) => {
|
| const numValue = parseFloat(value);
|
| if (isNaN(numValue) || numValue < 0) return;
|
|
|
| setPricingData((prev) => {
|
| const newData = { ...prev };
|
| if (!newData[provider]) newData[provider] = {};
|
| if (!newData[provider][model]) newData[provider][model] = {};
|
| newData[provider][model][field] = numValue;
|
| return newData;
|
| });
|
| };
|
|
|
| const handleSave = async () => {
|
| setSaving(true);
|
| try {
|
| const response = await fetch("/api/pricing", {
|
| method: "PATCH",
|
| headers: { "Content-Type": "application/json" },
|
| body: JSON.stringify(pricingData),
|
| });
|
|
|
| if (response.ok) {
|
| onSave?.();
|
| onClose();
|
| } else {
|
| const error = await response.json();
|
| alert(`${t("errorSaveFailed")}: ${error.error}`);
|
| }
|
| } catch (error) {
|
| console.error("Failed to save pricing:", error);
|
| alert(t("errorSaveFailed"));
|
| } finally {
|
| setSaving(false);
|
| }
|
| };
|
|
|
| const handleReset = async () => {
|
| if (!confirm(t("resetConfirm"))) return;
|
|
|
| try {
|
| const response = await fetch("/api/pricing", { method: "DELETE" });
|
| if (response.ok) {
|
| const defaults = getDefaultPricing();
|
| setPricingData(defaults);
|
| }
|
| } catch (error) {
|
| console.error("Failed to reset pricing:", error);
|
| alert(t("errorResetFailed"));
|
| }
|
| };
|
|
|
| if (!isOpen) return null;
|
|
|
|
|
| const allProviders = Object.keys(pricingData).sort();
|
| const pricingFields = ["input", "output", "cached", "reasoning", "cache_creation"];
|
|
|
| return (
|
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
| <div className="bg-bg-base border border-border rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
| {/* Header */}
|
| <div className="p-4 border-b border-border flex items-center justify-between">
|
| <h2 className="text-xl font-semibold">{t("title")}</h2>
|
| <button
|
| onClick={onClose}
|
| className="text-text-muted hover:text-text text-2xl leading-none"
|
| >
|
| ×
|
| </button>
|
| </div>
|
|
|
| {/* Content */}
|
| <div className="flex-1 overflow-auto p-4">
|
| {loading ? (
|
| <div className="text-center py-8 text-text-muted">{t("loading")}</div>
|
| ) : (
|
| <div className="space-y-6">
|
| {/* Instructions */}
|
| <div className="bg-bg-subtle border border-border rounded-lg p-3 text-sm">
|
| <p className="font-medium mb-1">{t("pricingRatesFormat")}</p>
|
| <p className="text-text-muted">
|
| {t.rich("ratesDescription", {
|
| strong: (c) => <strong className="font-semibold">{c}</strong>,
|
| })}
|
| </p>
|
| </div>
|
|
|
| {/* Pricing Tables */}
|
| {allProviders.map((provider) => {
|
| const models = Object.keys(pricingData[provider]).sort();
|
| return (
|
| <div key={provider} className="border border-border rounded-lg overflow-hidden">
|
| <div className="bg-bg-subtle px-4 py-2 font-semibold text-sm">
|
| {provider.toUpperCase()}
|
| </div>
|
| <div className="overflow-x-auto">
|
| <table className="w-full text-sm">
|
| <thead className="bg-bg-hover text-text-muted uppercase text-xs">
|
| <tr>
|
| <th className="px-3 py-2 text-left">{t("model")}</th>
|
| <th className="px-3 py-2 text-right">{t("input")}</th>
|
| <th className="px-3 py-2 text-right">{t("output")}</th>
|
| <th className="px-3 py-2 text-right">{t("cached")}</th>
|
| <th className="px-3 py-2 text-right">{t("reasoning")}</th>
|
| <th className="px-3 py-2 text-right">{t("cacheCreation")}</th>
|
| </tr>
|
| </thead>
|
| <tbody className="divide-y divide-border">
|
| {models.map((model) => (
|
| <tr key={model} className="hover:bg-bg-subtle/50">
|
| <td className="px-3 py-2 font-medium">{model}</td>
|
| {pricingFields.map((field) => (
|
| <td key={field} className="px-3 py-2">
|
| <input
|
| type="number"
|
| step="0.01"
|
| min="0"
|
| value={pricingData[provider][model][field] || 0}
|
| onChange={(e) =>
|
| handlePricingChange(provider, model, field, e.target.value)
|
| }
|
| className="w-20 px-2 py-1 text-right bg-bg-base border border-border rounded focus:outline-none focus:border-primary"
|
| />
|
| </td>
|
| ))}
|
| </tr>
|
| ))}
|
| </tbody>
|
| </table>
|
| </div>
|
| </div>
|
| );
|
| })}
|
|
|
| {allProviders.length === 0 && (
|
| <div className="text-center py-8 text-text-muted">{t("noPricingData")}</div>
|
| )}
|
| </div>
|
| )}
|
| </div>
|
|
|
| {/* Footer */}
|
| <div className="p-4 border-t border-border flex items-center justify-between gap-2">
|
| <button
|
| onClick={handleReset}
|
| className="px-4 py-2 text-sm text-red-500 hover:bg-red-500/10 rounded border border-red-500/20 transition-colors"
|
| disabled={saving}
|
| >
|
| {t("resetToDefaults")}
|
| </button>
|
| <div className="flex gap-2">
|
| <button
|
| onClick={onClose}
|
| className="px-4 py-2 text-sm text-text-muted hover:text-text border border-border rounded transition-colors"
|
| disabled={saving}
|
| >
|
| {t("cancel")}
|
| </button>
|
| <button
|
| onClick={handleSave}
|
| className="px-4 py-2 text-sm bg-primary text-white rounded hover:bg-primary/90 transition-colors disabled:opacity-50"
|
| disabled={saving}
|
| >
|
| {saving ? t("saving") : t("saveChanges")}
|
| </button>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| );
|
| }
|
|
|