2026-04-04 00:25:58 -04:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
|
import PropTypes from "prop-types";
|
|
|
|
|
import Modal from "@/shared/components/Modal";
|
|
|
|
|
import Input from "@/shared/components/Input";
|
|
|
|
|
import Button from "@/shared/components/Button";
|
|
|
|
|
import Badge from "@/shared/components/Badge";
|
2026-07-03 00:00:02 -04:00
|
|
|
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS } from "@/shared/constants/providers";
|
|
|
|
|
import Select from "@/shared/components/Select";
|
2026-04-04 00:25:58 -04:00
|
|
|
|
|
|
|
|
export default function EditConnectionModal({ isOpen, connection, proxyPools, onSave, onClose }) {
|
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
name: "",
|
|
|
|
|
priority: 1,
|
|
|
|
|
apiKey: "",
|
|
|
|
|
});
|
2026-04-23 23:04:59 -04:00
|
|
|
const [azureData, setAzureData] = useState({
|
|
|
|
|
azureEndpoint: "",
|
|
|
|
|
apiVersion: "2024-10-01-preview",
|
|
|
|
|
deployment: "",
|
|
|
|
|
organization: "",
|
|
|
|
|
});
|
2026-04-28 00:07:39 -04:00
|
|
|
const [cloudflareData, setCloudflareData] = useState({ accountId: "" });
|
2026-07-03 00:00:02 -04:00
|
|
|
const [region, setRegion] = useState("");
|
2026-04-04 00:25:58 -04:00
|
|
|
const [testing, setTesting] = useState(false);
|
|
|
|
|
const [testResult, setTestResult] = useState(null);
|
|
|
|
|
const [validating, setValidating] = useState(false);
|
|
|
|
|
const [validationResult, setValidationResult] = useState(null);
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (connection) {
|
|
|
|
|
setFormData({
|
|
|
|
|
name: connection.name || "",
|
|
|
|
|
priority: connection.priority || 1,
|
|
|
|
|
apiKey: "",
|
|
|
|
|
});
|
2026-04-23 23:04:59 -04:00
|
|
|
// Load Azure-specific data if present
|
|
|
|
|
if (connection.provider === "azure" && connection.providerSpecificData) {
|
|
|
|
|
setAzureData({
|
|
|
|
|
azureEndpoint: connection.providerSpecificData.azureEndpoint || "",
|
|
|
|
|
apiVersion: connection.providerSpecificData.apiVersion || "2024-10-01-preview",
|
|
|
|
|
deployment: connection.providerSpecificData.deployment || "",
|
|
|
|
|
organization: connection.providerSpecificData.organization || "",
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-28 00:07:39 -04:00
|
|
|
if (connection.provider === "cloudflare-ai" && connection.providerSpecificData) {
|
|
|
|
|
setCloudflareData({ accountId: connection.providerSpecificData.accountId || "" });
|
|
|
|
|
}
|
2026-07-03 00:00:02 -04:00
|
|
|
// Load region for providers that support it (e.g. xiaomi-tokenplan)
|
|
|
|
|
const providerCfg = AI_PROVIDERS?.[connection.provider];
|
|
|
|
|
if (providerCfg?.regions) {
|
|
|
|
|
const savedRegion = connection.providerSpecificData?.region || providerCfg.defaultRegion || providerCfg.regions[0]?.id || "";
|
|
|
|
|
setRegion(savedRegion);
|
|
|
|
|
}
|
2026-04-04 00:25:58 -04:00
|
|
|
setTestResult(null);
|
|
|
|
|
setValidationResult(null);
|
|
|
|
|
}
|
|
|
|
|
}, [connection]);
|
|
|
|
|
|
|
|
|
|
const isOAuth = connection?.authType === "oauth";
|
2026-04-23 23:04:59 -04:00
|
|
|
const isAzure = connection?.provider === "azure";
|
2026-04-28 00:07:39 -04:00
|
|
|
const isCloudflareAi = connection?.provider === "cloudflare-ai";
|
2026-04-04 00:25:58 -04:00
|
|
|
const isCompatible = connection
|
|
|
|
|
? (isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider))
|
|
|
|
|
: false;
|
2026-07-03 00:00:02 -04:00
|
|
|
const providerRegions = connection ? (AI_PROVIDERS?.[connection.provider]?.regions || null) : null;
|
|
|
|
|
|
|
|
|
|
// Build providerSpecificData for region-aware providers
|
|
|
|
|
const buildRegionSpecificData = () => {
|
|
|
|
|
if (providerRegions && region) return { ...((connection?.providerSpecificData) || {}), region };
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|
2026-04-04 00:25:58 -04:00
|
|
|
|
|
|
|
|
const handleTest = async () => {
|
|
|
|
|
if (!connection?.provider) return;
|
|
|
|
|
setTesting(true);
|
|
|
|
|
setTestResult(null);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" });
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setTestResult(data.valid ? "success" : "failed");
|
|
|
|
|
} catch {
|
|
|
|
|
setTestResult("failed");
|
|
|
|
|
} finally {
|
|
|
|
|
setTesting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleValidate = async () => {
|
|
|
|
|
if (!connection?.provider || !formData.apiKey) return;
|
|
|
|
|
setValidating(true);
|
|
|
|
|
setValidationResult(null);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/providers/validate", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
2026-04-23 23:04:59 -04:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
provider: connection.provider,
|
|
|
|
|
apiKey: formData.apiKey,
|
|
|
|
|
...(isAzure ? { providerSpecificData: azureData } : {}),
|
2026-04-28 00:07:39 -04:00
|
|
|
...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}),
|
2026-07-03 00:00:02 -04:00
|
|
|
...(providerRegions ? { providerSpecificData: buildRegionSpecificData() } : {}),
|
2026-04-23 23:04:59 -04:00
|
|
|
}),
|
2026-04-04 00:25:58 -04:00
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setValidationResult(data.valid ? "success" : "failed");
|
|
|
|
|
} catch {
|
|
|
|
|
setValidationResult("failed");
|
|
|
|
|
} finally {
|
|
|
|
|
setValidating(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
if (!connection) return;
|
|
|
|
|
setSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
const updates = {
|
|
|
|
|
name: formData.name,
|
|
|
|
|
priority: formData.priority,
|
|
|
|
|
};
|
|
|
|
|
if (!isOAuth && formData.apiKey) {
|
|
|
|
|
updates.apiKey = formData.apiKey;
|
|
|
|
|
let isValid = validationResult === "success";
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
try {
|
|
|
|
|
setValidating(true);
|
|
|
|
|
setValidationResult(null);
|
|
|
|
|
const res = await fetch("/api/providers/validate", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
2026-04-23 23:04:59 -04:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
provider: connection.provider,
|
|
|
|
|
apiKey: formData.apiKey,
|
|
|
|
|
...(isAzure ? { providerSpecificData: azureData } : {}),
|
2026-04-28 00:07:39 -04:00
|
|
|
...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}),
|
2026-07-03 00:00:02 -04:00
|
|
|
...(providerRegions ? { providerSpecificData: buildRegionSpecificData() } : {}),
|
2026-04-23 23:04:59 -04:00
|
|
|
}),
|
2026-04-04 00:25:58 -04:00
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
isValid = !!data.valid;
|
|
|
|
|
setValidationResult(isValid ? "success" : "failed");
|
|
|
|
|
} catch {
|
|
|
|
|
setValidationResult("failed");
|
|
|
|
|
} finally {
|
|
|
|
|
setValidating(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (isValid) {
|
|
|
|
|
updates.testStatus = "active";
|
|
|
|
|
updates.lastError = null;
|
|
|
|
|
updates.lastErrorAt = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-23 23:04:59 -04:00
|
|
|
|
|
|
|
|
// Add Azure-specific data if this is an Azure connection
|
|
|
|
|
if (isAzure) {
|
|
|
|
|
updates.providerSpecificData = {
|
|
|
|
|
azureEndpoint: azureData.azureEndpoint,
|
|
|
|
|
apiVersion: azureData.apiVersion,
|
|
|
|
|
deployment: azureData.deployment,
|
|
|
|
|
organization: azureData.organization,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-04-28 00:07:39 -04:00
|
|
|
if (isCloudflareAi) {
|
|
|
|
|
updates.providerSpecificData = { accountId: cloudflareData.accountId };
|
|
|
|
|
}
|
2026-07-03 00:00:02 -04:00
|
|
|
// Persist updated region for region-aware providers
|
|
|
|
|
if (providerRegions && region) {
|
|
|
|
|
updates.providerSpecificData = buildRegionSpecificData();
|
|
|
|
|
}
|
2026-04-23 23:04:59 -04:00
|
|
|
|
2026-04-04 00:25:58 -04:00
|
|
|
await onSave(updates);
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!connection) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal isOpen={isOpen} title="Edit Connection" onClose={onClose}>
|
|
|
|
|
<div className="flex flex-col gap-4">
|
|
|
|
|
<Input
|
|
|
|
|
label="Name"
|
|
|
|
|
value={formData.name}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
|
|
|
placeholder={isOAuth ? "Account name" : "Production Key"}
|
|
|
|
|
/>
|
|
|
|
|
{isOAuth && connection.email && (
|
|
|
|
|
<div className="bg-sidebar/50 p-3 rounded-lg">
|
|
|
|
|
<p className="text-sm text-text-muted mb-1">Email</p>
|
|
|
|
|
<p className="font-medium">{connection.email}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<Input
|
|
|
|
|
label="Priority"
|
|
|
|
|
type="number"
|
|
|
|
|
value={formData.priority}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value, 10) || 1 })}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{!isOAuth && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
label="API Key"
|
|
|
|
|
type="password"
|
|
|
|
|
value={formData.apiKey}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
|
|
|
|
placeholder="Enter new API key"
|
|
|
|
|
hint="Leave blank to keep the current API key."
|
|
|
|
|
className="flex-1"
|
|
|
|
|
/>
|
|
|
|
|
<div className="pt-6">
|
|
|
|
|
<Button onClick={handleValidate} disabled={!formData.apiKey || validating || saving} variant="secondary">
|
|
|
|
|
{validating ? "Checking..." : "Check"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{validationResult && (
|
|
|
|
|
<Badge variant={validationResult === "success" ? "success" : "error"}>
|
|
|
|
|
{validationResult === "success" ? "Valid" : "Invalid"}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-23 23:04:59 -04:00
|
|
|
{isAzure && (
|
|
|
|
|
<div className="bg-sidebar/50 p-4 rounded-lg border border-accent/20">
|
|
|
|
|
<h3 className="font-semibold mb-3 text-sm">Azure OpenAI Configuration</h3>
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
<Input
|
|
|
|
|
label="Azure Endpoint"
|
|
|
|
|
value={azureData.azureEndpoint}
|
|
|
|
|
onChange={(e) => setAzureData({ ...azureData, azureEndpoint: e.target.value })}
|
|
|
|
|
placeholder="https://your-resource.openai.azure.com"
|
|
|
|
|
hint="Your Azure OpenAI resource endpoint URL"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Deployment Name"
|
|
|
|
|
value={azureData.deployment}
|
|
|
|
|
onChange={(e) => setAzureData({ ...azureData, deployment: e.target.value })}
|
|
|
|
|
placeholder="gpt-4"
|
|
|
|
|
hint="The deployment name in your Azure resource"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="API Version"
|
|
|
|
|
value={azureData.apiVersion}
|
|
|
|
|
onChange={(e) => setAzureData({ ...azureData, apiVersion: e.target.value })}
|
|
|
|
|
placeholder="2024-10-01-preview"
|
|
|
|
|
hint="Azure OpenAI API version to use"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
label="Organization"
|
|
|
|
|
value={azureData.organization}
|
|
|
|
|
onChange={(e) => setAzureData({ ...azureData, organization: e.target.value })}
|
|
|
|
|
placeholder="Organization ID"
|
|
|
|
|
hint="Required for billing"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-07-03 00:00:02 -04:00
|
|
|
{providerRegions && (
|
|
|
|
|
<Select
|
|
|
|
|
label="Region"
|
|
|
|
|
value={region}
|
|
|
|
|
onChange={(e) => setRegion(e.target.value)}
|
|
|
|
|
options={providerRegions.map((r) => ({ value: r.id, label: r.label }))}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-28 00:07:39 -04:00
|
|
|
{!isCompatible && !isAzure && !isCloudflareAi && (
|
2026-04-04 00:25:58 -04:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Button onClick={handleTest} variant="secondary" disabled={testing}>
|
|
|
|
|
{testing ? "Testing..." : "Test Connection"}
|
|
|
|
|
</Button>
|
|
|
|
|
{testResult && (
|
|
|
|
|
<Badge variant={testResult === "success" ? "success" : "error"}>
|
|
|
|
|
{testResult === "success" ? "Valid" : "Failed"}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button onClick={handleSubmit} fullWidth disabled={saving}>{saving ? "Saving..." : "Save"}</Button>
|
|
|
|
|
<Button onClick={onClose} variant="ghost" fullWidth>Cancel</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EditConnectionModal.propTypes = {
|
|
|
|
|
isOpen: PropTypes.bool.isRequired,
|
|
|
|
|
connection: PropTypes.shape({
|
|
|
|
|
id: PropTypes.string,
|
|
|
|
|
name: PropTypes.string,
|
|
|
|
|
email: PropTypes.string,
|
|
|
|
|
priority: PropTypes.number,
|
|
|
|
|
authType: PropTypes.string,
|
|
|
|
|
provider: PropTypes.string,
|
|
|
|
|
providerSpecificData: PropTypes.object,
|
|
|
|
|
}),
|
|
|
|
|
proxyPools: PropTypes.arrayOf(PropTypes.shape({
|
|
|
|
|
id: PropTypes.string,
|
|
|
|
|
name: PropTypes.string,
|
|
|
|
|
})),
|
|
|
|
|
onSave: PropTypes.func.isRequired,
|
|
|
|
|
onClose: PropTypes.func.isRequired,
|
|
|
|
|
};
|
2026-04-23 23:04:59 -04:00
|
|
|
|