Fix MITM on window
This commit is contained in:
1 parent
49a56612bf
commit
833069caac
22 files changed
+650
-199
No files matched your search
@@ -240,7 +240,23 @@ export default function AntigravityToolCard({
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-border flex flex-col gap-4">
|
||||
{/* Start/Stop Button - always on top */}
|
||||
{/* Status indicators */}
|
||||
<div className="flex items-center gap-3">
|
||||
{[
|
||||
{ label: "DNS", ok: status?.dnsConfigured },
|
||||
{ label: "Cert", ok: status?.certExists },
|
||||
{ label: "Server", ok: status?.running },
|
||||
].map(({ label, ok }) => (
|
||||
<div key={label} className="flex items-center gap-1">
|
||||
<span className={`material-symbols-outlined text-[14px] ${ok ? "text-green-500" : "text-text-muted"}`}>
|
||||
{ok ? "check_circle" : "radio_button_unchecked"}
|
||||
</span>
|
||||
<span className={`text-xs ${ok ? "text-green-500" : "text-text-muted"}`}>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Start/Stop Button */}
|
||||
<div className="flex items-center gap-2">
|
||||
{isRunning ? (
|
||||
<button
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function ProviderDetailPage() {
|
||||
const [headerImgError, setHeaderImgError] = useState(false);
|
||||
const [modelTestResults, setModelTestResults] = useState({});
|
||||
const [testingModels, setTestingModels] = useState(false);
|
||||
const [showAddCustomModel, setShowAddCustomModel] = useState(false);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
|
||||
const providerInfo = providerNode
|
||||
@@ -307,9 +308,21 @@ export default function ProviderDetailPage() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (models.length === 0) {
|
||||
return <p className="text-sm text-text-muted">No models configured</p>;
|
||||
}
|
||||
// Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
|
||||
const customModels = Object.entries(modelAliases)
|
||||
.filter(([alias, fullModel]) => {
|
||||
const prefix = `${providerStorageAlias}/`;
|
||||
if (!fullModel.startsWith(prefix)) return false;
|
||||
const modelId = fullModel.slice(prefix.length);
|
||||
// Only show if not already in hardcoded list
|
||||
return !models.some((m) => m.id === modelId) && alias === modelId;
|
||||
})
|
||||
.map(([alias, fullModel]) => ({
|
||||
id: fullModel.slice(`${providerStorageAlias}/`.length),
|
||||
alias,
|
||||
fullModel,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{models.map((model) => {
|
||||
@@ -332,6 +345,30 @@ export default function ProviderDetailPage() {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Custom models inline */}
|
||||
{customModels.map((model) => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={{ id: model.id }}
|
||||
fullModel={`${providerDisplayAlias}/${model.id}`}
|
||||
alias={model.alias}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onSetAlias={() => {}}
|
||||
onDeleteAlias={() => handleDeleteAlias(model.alias)}
|
||||
isCustom
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Add model button — inline, same style as model chips */}
|
||||
<button
|
||||
onClick={() => setShowAddCustomModel(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-dashed border-black/15 dark:border-white/15 text-xs text-text-muted hover:text-primary hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">add</span>
|
||||
Add Model
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -526,7 +563,8 @@ export default function ProviderDetailPage() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={testingModels ? "progress_activity" : "science"}
|
||||
icon="science"
|
||||
loading={testingModels}
|
||||
onClick={handleTestModels}
|
||||
disabled={testingModels}
|
||||
>
|
||||
@@ -584,11 +622,23 @@ export default function ProviderDetailPage() {
|
||||
isAnthropic={isAnthropicCompatible}
|
||||
/>
|
||||
)}
|
||||
{!isCompatible && !providerInfo?.passthroughModels && (
|
||||
<AddCustomModelModal
|
||||
isOpen={showAddCustomModel}
|
||||
providerAlias={providerStorageAlias}
|
||||
providerDisplayAlias={providerDisplayAlias}
|
||||
onSave={async (modelId) => {
|
||||
await handleSetAlias(modelId, modelId, providerStorageAlias);
|
||||
setShowAddCustomModel(false);
|
||||
}}
|
||||
onClose={() => setShowAddCustomModel(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus }) {
|
||||
function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, onDeleteAlias }) {
|
||||
const borderColor = testStatus === "ok"
|
||||
? "border-green-500/40"
|
||||
: testStatus === "error"
|
||||
@@ -602,7 +652,7 @@ function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus }) {
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
|
||||
<div className={`group flex items-center gap-2 px-3 py-2 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
|
||||
<span
|
||||
className="material-symbols-outlined text-base"
|
||||
style={iconColor ? { color: iconColor } : undefined}
|
||||
@@ -619,6 +669,15 @@ function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus }) {
|
||||
{copied === `model-${model.id}` ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
{isCustom && (
|
||||
<button
|
||||
onClick={onDeleteAlias}
|
||||
className="p-0.5 hover:bg-red-500/10 rounded text-text-muted hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
|
||||
title="Remove custom model"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">close</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -632,6 +691,8 @@ ModelRow.propTypes = {
|
||||
copied: PropTypes.string,
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
testStatus: PropTypes.oneOf(["ok", "error"]),
|
||||
isCustom: PropTypes.bool,
|
||||
onDeleteAlias: PropTypes.func,
|
||||
};
|
||||
|
||||
function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) {
|
||||
@@ -1553,3 +1614,115 @@ EditCompatibleNodeModal.propTypes = {
|
||||
isAnthropic: PropTypes.bool,
|
||||
};
|
||||
|
||||
function AddCustomModelModal({ isOpen, providerAlias, providerDisplayAlias, onSave, onClose }) {
|
||||
const [modelId, setModelId] = useState("");
|
||||
const [testStatus, setTestStatus] = useState(null); // null | "testing" | "ok" | "error"
|
||||
const [testError, setTestError] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) { setModelId(""); setTestStatus(null); setTestError(""); }
|
||||
}, [isOpen]);
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!modelId.trim()) return;
|
||||
setTestStatus("testing");
|
||||
setTestError("");
|
||||
try {
|
||||
const res = await fetch("/api/models/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: `${providerAlias}/${modelId.trim()}` }),
|
||||
});
|
||||
const data = await res.json();
|
||||
setTestStatus(data.ok ? "ok" : "error");
|
||||
setTestError(data.error || "");
|
||||
} catch (err) {
|
||||
setTestStatus("error");
|
||||
setTestError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!modelId.trim() || saving) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave(modelId.trim());
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === "Enter") handleTest();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Add Custom Model">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">Model ID</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={modelId}
|
||||
onChange={(e) => { setModelId(e.target.value); setTestStatus(null); setTestError(""); }}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="e.g. claude-opus-4-5"
|
||||
className="flex-1 px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="science"
|
||||
loading={testStatus === "testing"}
|
||||
onClick={handleTest}
|
||||
disabled={!modelId.trim() || testStatus === "testing"}
|
||||
>
|
||||
{testStatus === "testing" ? "Testing..." : "Test"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Sent to provider as: <code className="font-mono bg-sidebar px-1 rounded">{modelId.trim() || "model-id"}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Test result */}
|
||||
{testStatus === "ok" && (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600">
|
||||
<span className="material-symbols-outlined text-base">check_circle</span>
|
||||
Model is reachable
|
||||
</div>
|
||||
)}
|
||||
{testStatus === "error" && (
|
||||
<div className="flex items-start gap-2 text-sm text-red-500">
|
||||
<span className="material-symbols-outlined text-base shrink-0">cancel</span>
|
||||
<span>{testError || "Model not reachable"}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button onClick={onClose} variant="ghost" fullWidth size="sm">Cancel</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
fullWidth
|
||||
size="sm"
|
||||
disabled={!modelId.trim() || saving}
|
||||
>
|
||||
{saving ? "Adding..." : "Add Model"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddCustomModelModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
providerAlias: PropTypes.string.isRequired,
|
||||
providerDisplayAlias: PropTypes.string.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -222,7 +222,7 @@ export default function ProvidersPage() {
|
||||
title="Test all OAuth connections"
|
||||
aria-label="Test all OAuth connections"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "oauth" ? " animate-spin" : ""}`}>
|
||||
{testingMode === "oauth" ? "sync" : "play_arrow"}
|
||||
</span>
|
||||
{testingMode === "oauth" ? "Testing..." : "Test All"}
|
||||
@@ -260,7 +260,7 @@ export default function ProvidersPage() {
|
||||
title="Test all Free connections"
|
||||
aria-label="Test all Free provider connections"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "free" ? " animate-spin" : ""}`}>
|
||||
{testingMode === "free" ? "sync" : "play_arrow"}
|
||||
</span>
|
||||
{testingMode === "free" ? "Testing..." : "Test All"}
|
||||
@@ -297,7 +297,7 @@ export default function ProvidersPage() {
|
||||
title="Test all API Key connections"
|
||||
aria-label="Test all API Key connections"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "apikey" ? " animate-spin" : ""}`}>
|
||||
{testingMode === "apikey" ? "sync" : "play_arrow"}
|
||||
</span>
|
||||
{testingMode === "apikey" ? "Testing..." : "Test All"}
|
||||
@@ -335,7 +335,7 @@ export default function ProvidersPage() {
|
||||
}`}
|
||||
title="Test all Compatible connections"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "compatible" ? " animate-spin" : ""}`}>
|
||||
{testingMode === "compatible" ? "sync" : "play_arrow"}
|
||||
</span>
|
||||
{testingMode === "compatible" ? "Testing..." : "Test All"}
|
||||
|
||||
@@ -144,7 +144,7 @@ function buildLayout(providers, activeSet, lastSet, errorSet) {
|
||||
const error = !active && errorSet.has(p.provider?.toLowerCase());
|
||||
const nodeId = `provider-${p.provider}`;
|
||||
const data = {
|
||||
label: config.name || p.name || p.provider,
|
||||
label: (config.name !== p.provider ? config.name : null) || p.name || p.provider,
|
||||
color: config.color || "#6b7280",
|
||||
imageUrl: getProviderImageUrl(p.provider),
|
||||
textIcon: config.textIcon || (p.provider || "?").slice(0, 2).toUpperCase(),
|
||||
|
||||
Reference in new issue
Block a user