ai_api / src /app /(dashboard) /dashboard /settings /components /CliproxyapiSettingsTab.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
10.8 kB
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { Card, Button, Input, Toggle } from "@/shared/components";
interface Settings {
cliproxyapi_fallback_enabled?: boolean;
cliproxyapi_url?: string;
cliproxyapi_fallback_codes?: string;
[key: string]: unknown;
}
interface VersionManagerEntry {
tool: string;
status: string;
installedVersion: string | null;
healthStatus: string;
port: number;
}
function isValidUrl(value: string): boolean {
try {
const url = new URL(value);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
export default function CliproxyapiSettingsTab() {
const t = useTranslations("settings");
const [settings, setSettings] = useState<Settings>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: string; text: string } | null>(null);
const [toolState, setToolState] = useState<VersionManagerEntry | null>(null);
const [toolStateError, setToolStateError] = useState<string | null>(null);
// #1934: import CLIProxyAPI auth files (~/.cli-proxy-api/) as OmniRoute connections.
const [importing, setImporting] = useState(false);
const [importResult, setImportResult] = useState<string | null>(null);
const handleImportAuth = useCallback(async () => {
setImporting(true);
setImportResult(null);
try {
const res = await fetch("/api/oauth/cliproxy-import", { method: "POST" });
const data = await res.json();
if (res.ok) {
setImportResult(
`Imported ${data.imported ?? 0} account(s) (scanned ${data.scanned ?? 0}, skipped ${data.skipped ?? 0}).`
);
} else {
setImportResult(data.error || "Import failed.");
}
} catch {
setImportResult("Import failed.");
} finally {
setImporting(false);
}
}, []);
useEffect(() => {
fetch("/api/settings")
.then((r) => {
if (!r.ok) throw new Error(`Settings API returned ${r.status}`);
return r.json();
})
.then((data) => {
setSettings(data);
setLoading(false);
})
.catch((err) => {
console.error("Failed to load settings:", err);
setLoading(false);
});
fetch("/api/version-manager/status")
.then((r) => {
if (!r.ok) throw new Error(`Version manager API returned ${r.status}`);
return r.json();
})
.then((data) => {
const entry = Array.isArray(data)
? data.find((t: VersionManagerEntry) => t.tool === "cliproxyapi")
: null;
setToolState(entry ?? null);
setToolStateError(null);
})
.catch((err) => {
console.error("Failed to load version manager status:", err);
setToolStateError("Unable to reach version manager service");
setToolState(null);
});
}, []);
const updateSetting = useCallback(async (key: string, value: boolean | string) => {
if (key === "cliproxyapi_url" && typeof value === "string" && value.trim() !== "") {
if (!isValidUrl(value)) {
setMessage({ type: "error", text: "Invalid URL format. Use http:// or https://" });
return;
}
}
setSaving(true);
setMessage(null);
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ [key]: value }),
});
if (!res.ok) {
throw new Error(`Server returned ${res.status}`);
}
await res.json();
setSettings((prev) => ({ ...prev, [key]: value }));
setMessage({ type: "success", text: "Setting saved" });
} catch {
setMessage({ type: "error", text: "Failed to save setting" });
} finally {
setSaving(false);
}
}, []);
const cpaEnabled = settings.cliproxyapi_fallback_enabled === true;
const cpaUrl = settings.cliproxyapi_url || "http://127.0.0.1:8317";
const cpaCodes = settings.cliproxyapi_fallback_codes || "502,401,403,429,503";
const statusColor =
toolState?.status === "running"
? "text-green-600 dark:text-green-400"
: toolState?.status === "error"
? "text-red-600 dark:text-red-400"
: "text-text-muted";
const statusIcon =
toolState?.status === "running"
? "check_circle"
: toolState?.status === "error"
? "error"
: "help";
return (
<div className="space-y-4">
{/* Migration banner — new lifecycle management lives in the Services page */}
<div className="flex items-start gap-2 px-3 py-2.5 rounded-lg bg-blue-500/10 text-blue-700 dark:text-blue-300 text-xs">
<span className="material-symbols-outlined text-[14px] mt-0.5 shrink-0">info</span>
<span>
CLIProxyAPI lifecycle management (install, start, stop) has moved to{" "}
<Link
href="/dashboard/providers/services"
className="underline underline-offset-2 hover:opacity-80"
>
Providers → Services
</Link>
. Fallback routing settings below remain here.
</span>
</div>
{message && (
<div
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs ${
message.type === "success"
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-red-500/10 text-red-600 dark:text-red-400"
}`}
>
<span className="material-symbols-outlined text-[14px]">
{message.type === "success" ? "check_circle" : "error"}
</span>
{message.text}
</div>
)}
<Card padding="md">
<div className="flex items-center gap-3 mb-4">
<div className="size-8 rounded-lg flex items-center justify-center bg-indigo-500/10">
<span className="material-symbols-outlined text-indigo-500 text-xl">swap_horiz</span>
</div>
<div>
<h3 className="font-medium text-sm">{t("cliproxyapiFallback")}</h3>
<p className="text-xs text-text-muted">
When enabled, failed requests are retried through CLIProxyAPI (localhost:8317)
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-sm text-text-main">{t("cliproxyapiEnableFallback")}</label>
<Toggle
checked={cpaEnabled}
onChange={(checked) => updateSetting("cliproxyapi_fallback_enabled", checked)}
/>
</div>
{cpaEnabled && (
<>
<div>
<label className="text-xs text-text-muted mb-1.5 block">
{t("cliproxyapiUrl")}
</label>
<Input
value={cpaUrl}
onChange={(e) => updateSetting("cliproxyapi_url", e.target.value)}
placeholder="http://127.0.0.1:8317"
className="w-full"
/>
</div>
<div>
<label className="text-xs text-text-muted mb-1.5 block">
Fallback Status Codes (comma-separated)
</label>
<Input
value={cpaCodes}
onChange={(e) => updateSetting("cliproxyapi_fallback_codes", e.target.value)}
placeholder="502,401,403,429,503"
className="w-full"
/>
</div>
</>
)}
</div>
</Card>
<Card padding="md">
<h3 className="font-medium text-sm mb-4">{t("cliproxyapiStatus")}</h3>
{loading ? (
<div className="flex items-center gap-2 text-text-muted text-sm">
<span className="material-symbols-outlined animate-spin text-base">
progress_activity
</span>
Loading...
</div>
) : toolStateError ? (
<p className="text-sm text-text-muted">{toolStateError}</p>
) : toolState ? (
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Status</p>
<div className="flex items-center gap-1.5">
<span className={`material-symbols-outlined text-sm ${statusColor}`}>
{statusIcon}
</span>
<p className={`text-sm font-medium capitalize ${statusColor}`}>
{toolState.status?.replace("_", " ") || "Unknown"}
</p>
</div>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Version</p>
<p className="text-sm font-medium">
{toolState.installedVersion ? `v${toolState.installedVersion}` : "Not installed"}
</p>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Health</p>
<p
className={`text-sm font-medium ${
toolState.healthStatus === "healthy"
? "text-green-600 dark:text-green-400"
: toolState.healthStatus === "unhealthy"
? "text-red-600 dark:text-red-400"
: "text-text-muted"
}`}
>
{toolState.healthStatus === "healthy"
? "Healthy"
: toolState.healthStatus === "unhealthy"
? "Unhealthy"
: "Unknown"}
</p>
</div>
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-muted mb-1">Port</p>
<p className="text-sm font-mono">{toolState.port || 8317}</p>
</div>
</div>
) : (
<p className="text-sm text-text-muted">{t("cliproxyapiNotDetected")}</p>
)}
</Card>
<Card padding="md">
<h3 className="text-lg font-semibold mb-1">{t("cliproxyapiImportAuthTitle")}</h3>
<p className="text-sm text-text-muted mb-3">{t("cliproxyapiImportAuthDesc")}</p>
<Button onClick={handleImportAuth} loading={importing} disabled={importing}>
{t("cliproxyapiImportAuthButton")}
</Button>
{importResult ? (
<p className="text-sm text-text-muted mt-3" role="status">
{importResult}
</p>
) : null}
</Card>
</div>
);
}