mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
refactor(hermes): delegate deep config to Hermes Web UI
Slim the Hermes surface in CC Switch to match its core positioning — cross-client provider switching and shared MCP/prompts/skills — and delegate deep configuration (model, agent, env, skills, cron, logs) to the Hermes Web UI at http://127.0.0.1:9119. - Drop AgentPanel/EnvPanel/ModelPanel and their mutation commands, hooks, types, and i18n keys across zh/en/ja. - Add open_hermes_web_ui Tauri command that probes /api/status and launches the URL in the system browser. Hermes injects its own session token into the returned HTML, so CC Switch doesn't need to touch auth. - Surface the launcher from the Hermes toolbar and the health banner via a shared useOpenHermesWebUI() hook; the offline error code is defined once per side and referenced across the contract. - Keep read-only access to model.provider so ProviderList can still highlight the active supplier; apply_switch_defaults continues to write the top-level model section when switching providers. Net diff: +152 / -1253.
This commit is contained in:
@@ -1,249 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useHermesAgentConfig,
|
||||
useSaveHermesAgentConfig,
|
||||
} from "@/hooks/useHermes";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { HermesAgentConfig } from "@/types";
|
||||
|
||||
const UNSET_SENTINEL = "__unset__";
|
||||
|
||||
const REASONING_EFFORT_OPTIONS = [
|
||||
{ value: UNSET_SENTINEL, labelKey: "hermes.agent.notSet" },
|
||||
{ value: "none", label: "None" },
|
||||
{ value: "minimal", label: "Minimal" },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
{ value: "xhigh", label: "Extra High" },
|
||||
] as const;
|
||||
|
||||
const APPROVALS_MODE_OPTIONS = [
|
||||
{ value: UNSET_SENTINEL, labelKey: "hermes.agent.notSet" },
|
||||
{ value: "manual", label: "Manual" },
|
||||
{ value: "smart", label: "Smart" },
|
||||
{ value: "off", label: "Off" },
|
||||
] as const;
|
||||
|
||||
const AgentPanel: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: agentData, isLoading } = useHermesAgentConfig();
|
||||
const saveAgentMutation = useSaveHermesAgentConfig();
|
||||
|
||||
const [maxTurns, setMaxTurns] = useState("");
|
||||
const [reasoningEffort, setReasoningEffort] = useState(UNSET_SENTINEL);
|
||||
const [toolUseEnforcement, setToolUseEnforcement] = useState("");
|
||||
const [approvalsMode, setApprovalsMode] = useState(UNSET_SENTINEL);
|
||||
|
||||
// Preserve unknown fields
|
||||
const [extra, setExtra] = useState<Record<string, unknown>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (agentData === undefined) return;
|
||||
if (agentData) {
|
||||
setMaxTurns(
|
||||
agentData.max_turns != null ? String(agentData.max_turns) : "",
|
||||
);
|
||||
setReasoningEffort(agentData.reasoning_effort ?? UNSET_SENTINEL);
|
||||
setToolUseEnforcement(
|
||||
agentData.tool_use_enforcement != null
|
||||
? typeof agentData.tool_use_enforcement === "string"
|
||||
? agentData.tool_use_enforcement
|
||||
: JSON.stringify(agentData.tool_use_enforcement)
|
||||
: "",
|
||||
);
|
||||
setApprovalsMode(agentData.approvals_mode ?? UNSET_SENTINEL);
|
||||
const {
|
||||
max_turns: _mt,
|
||||
reasoning_effort: _re,
|
||||
tool_use_enforcement: _tu,
|
||||
approvals_mode: _am,
|
||||
...rest
|
||||
} = agentData;
|
||||
setExtra(rest);
|
||||
} else {
|
||||
setMaxTurns("");
|
||||
setReasoningEffort(UNSET_SENTINEL);
|
||||
setToolUseEnforcement("");
|
||||
setApprovalsMode(UNSET_SENTINEL);
|
||||
setExtra({});
|
||||
}
|
||||
}, [agentData]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const config: HermesAgentConfig = {
|
||||
...extra,
|
||||
};
|
||||
|
||||
const mt = parseInt(maxTurns);
|
||||
if (!isNaN(mt) && mt > 0) config.max_turns = mt;
|
||||
|
||||
if (reasoningEffort !== UNSET_SENTINEL) {
|
||||
config.reasoning_effort = reasoningEffort;
|
||||
}
|
||||
|
||||
if (toolUseEnforcement.trim()) {
|
||||
// Try parsing as JSON (for boolean/array values)
|
||||
try {
|
||||
config.tool_use_enforcement = JSON.parse(toolUseEnforcement.trim());
|
||||
} catch {
|
||||
config.tool_use_enforcement = toolUseEnforcement.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (approvalsMode !== UNSET_SENTINEL) {
|
||||
config.approvals_mode = approvalsMode;
|
||||
}
|
||||
|
||||
await saveAgentMutation.mutateAsync(config);
|
||||
toast.success(t("hermes.agent.saveSuccess"));
|
||||
} catch (error) {
|
||||
toast.error(t("hermes.agent.saveFailed"), {
|
||||
description: extractErrorMessage(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("hermes.agent.description")}
|
||||
</p>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card p-5 mb-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-agent-maxturns">
|
||||
{t("hermes.agent.maxTurns", { defaultValue: "Max Turns" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-agent-maxturns"
|
||||
type="number"
|
||||
value={maxTurns}
|
||||
onChange={(e) => setMaxTurns(e.target.value)}
|
||||
placeholder="100"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.agent.maxTurnsHint", {
|
||||
defaultValue: "Maximum number of agent turns per session",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-agent-reasoning">
|
||||
{t("hermes.agent.reasoningEffort", {
|
||||
defaultValue: "Reasoning Effort",
|
||||
})}
|
||||
</Label>
|
||||
<Select value={reasoningEffort} onValueChange={setReasoningEffort}>
|
||||
<SelectTrigger id="hermes-agent-reasoning">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{REASONING_EFFORT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{"label" in opt
|
||||
? opt.label
|
||||
: t(opt.labelKey, { defaultValue: "Not set" })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.agent.reasoningEffortHint", {
|
||||
defaultValue:
|
||||
"Controls the depth of reasoning: none, minimal, low, medium, high, xhigh",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-agent-tooluse">
|
||||
{t("hermes.agent.toolUseEnforcement", {
|
||||
defaultValue: "Tool Use Enforcement",
|
||||
})}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-agent-tooluse"
|
||||
value={toolUseEnforcement}
|
||||
onChange={(e) => setToolUseEnforcement(e.target.value)}
|
||||
placeholder="auto"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.agent.toolUseHint", {
|
||||
defaultValue:
|
||||
'Values: "auto", true, false, or a JSON array of tool names',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-agent-approvals">
|
||||
{t("hermes.agent.approvalsMode", {
|
||||
defaultValue: "Approvals Mode",
|
||||
})}
|
||||
</Label>
|
||||
<Select value={approvalsMode} onValueChange={setApprovalsMode}>
|
||||
<SelectTrigger id="hermes-agent-approvals">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{APPROVALS_MODE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{"label" in opt
|
||||
? opt.label
|
||||
: t(opt.labelKey, { defaultValue: "Not set" })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.agent.approvalsModeHint", {
|
||||
defaultValue:
|
||||
"Controls tool call approval: manual (always ask), smart (auto-approve safe), off (never ask)",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={saveAgentMutation.isPending}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{saveAgentMutation.isPending ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentPanel;
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useHermesEnv, useSaveHermesEnv } from "@/hooks/useHermes";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
|
||||
function parseEnvEditorValue(raw: string): Record<string, unknown> {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) throw new Error("HERMES_ENV_EMPTY");
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch {
|
||||
throw new Error("HERMES_ENV_INVALID_JSON");
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error("HERMES_ENV_OBJECT_REQUIRED");
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const EnvPanel: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: envData, isLoading } = useHermesEnv();
|
||||
const saveEnvMutation = useSaveHermesEnv();
|
||||
const [editorValue, setEditorValue] = useState("{}");
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const nextValue =
|
||||
envData && Object.keys(envData).length > 0
|
||||
? JSON.stringify(envData, null, 2)
|
||||
: "{}";
|
||||
setEditorValue(nextValue);
|
||||
}, [envData]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const env = parseEnvEditorValue(editorValue);
|
||||
await saveEnvMutation.mutateAsync(env);
|
||||
toast.success(t("hermes.env.saveSuccess"));
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
let description = detail || undefined;
|
||||
if (detail === "HERMES_ENV_EMPTY") {
|
||||
description = t("hermes.env.empty", {
|
||||
defaultValue:
|
||||
"Hermes env cannot be empty. Use {} for an empty object.",
|
||||
});
|
||||
} else if (detail === "HERMES_ENV_INVALID_JSON") {
|
||||
description = t("hermes.env.invalidJson", {
|
||||
defaultValue: "Hermes env must be valid JSON.",
|
||||
});
|
||||
} else if (detail === "HERMES_ENV_OBJECT_REQUIRED") {
|
||||
description = t("hermes.env.objectRequired", {
|
||||
defaultValue: "Hermes env must be a JSON object.",
|
||||
});
|
||||
}
|
||||
toast.error(t("hermes.env.saveFailed"), {
|
||||
description,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("hermes.env.description")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
{t("hermes.env.editorHint", {
|
||||
defaultValue:
|
||||
"Edit the Hermes .env file as a JSON key-value map. Keys become environment variable names.",
|
||||
})}
|
||||
</p>
|
||||
|
||||
<JsonEditor
|
||||
value={editorValue}
|
||||
onChange={setEditorValue}
|
||||
darkMode={isDarkMode}
|
||||
rows={18}
|
||||
showValidation={true}
|
||||
language="json"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={saveEnvMutation.isPending}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{saveEnvMutation.isPending ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvPanel;
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import { ExternalLink, TriangleAlert } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useOpenHermesWebUI } from "@/hooks/useHermes";
|
||||
import type { HermesHealthWarning } from "@/types";
|
||||
|
||||
interface HermesHealthBannerProps {
|
||||
@@ -37,6 +39,7 @@ const HermesHealthBanner: React.FC<HermesHealthBannerProps> = ({
|
||||
warnings,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const openHermesWebUI = useOpenHermesWebUI();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
@@ -55,10 +58,21 @@ const HermesHealthBanner: React.FC<HermesHealthBannerProps> = ({
|
||||
<div className="px-6 pt-4">
|
||||
<Alert className="border-amber-500/30 bg-amber-500/5">
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{t("hermes.health.title", {
|
||||
defaultValue: "Hermes config warnings detected",
|
||||
})}
|
||||
<AlertTitle className="flex items-center justify-between gap-2">
|
||||
<span>
|
||||
{t("hermes.health.title", {
|
||||
defaultValue: "Hermes config warnings detected",
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void openHermesWebUI("/config")}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5 mr-1" />
|
||||
{t("hermes.webui.fixInWebUI")}
|
||||
</Button>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useHermesModelConfig,
|
||||
useSaveHermesModelConfig,
|
||||
} from "@/hooks/useHermes";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { HermesModelConfig } from "@/types";
|
||||
|
||||
const ModelPanel: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: modelData, isLoading } = useHermesModelConfig(true);
|
||||
const saveModelMutation = useSaveHermesModelConfig();
|
||||
|
||||
const [defaultModel, setDefaultModel] = useState("");
|
||||
const [provider, setProvider] = useState("");
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [contextLength, setContextLength] = useState("");
|
||||
const [maxTokens, setMaxTokens] = useState("");
|
||||
|
||||
// Preserve unknown fields from the original config
|
||||
const [extra, setExtra] = useState<Record<string, unknown>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (modelData === undefined) return;
|
||||
if (modelData) {
|
||||
setDefaultModel(modelData.default ?? "");
|
||||
setProvider(modelData.provider ?? "");
|
||||
setBaseUrl(modelData.base_url ?? "");
|
||||
setContextLength(
|
||||
modelData.context_length != null
|
||||
? String(modelData.context_length)
|
||||
: "",
|
||||
);
|
||||
setMaxTokens(
|
||||
modelData.max_tokens != null ? String(modelData.max_tokens) : "",
|
||||
);
|
||||
// Collect unknown fields
|
||||
const {
|
||||
default: _d,
|
||||
provider: _p,
|
||||
base_url: _b,
|
||||
context_length: _c,
|
||||
max_tokens: _m,
|
||||
...rest
|
||||
} = modelData;
|
||||
setExtra(rest);
|
||||
} else {
|
||||
setDefaultModel("");
|
||||
setProvider("");
|
||||
setBaseUrl("");
|
||||
setContextLength("");
|
||||
setMaxTokens("");
|
||||
setExtra({});
|
||||
}
|
||||
}, [modelData]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const config: HermesModelConfig = {
|
||||
...extra,
|
||||
};
|
||||
if (defaultModel.trim()) config.default = defaultModel.trim();
|
||||
if (provider.trim()) config.provider = provider.trim();
|
||||
if (baseUrl.trim()) config.base_url = baseUrl.trim();
|
||||
|
||||
const cl = parseInt(contextLength);
|
||||
if (!isNaN(cl) && cl > 0) config.context_length = cl;
|
||||
|
||||
const mt = parseInt(maxTokens);
|
||||
if (!isNaN(mt) && mt > 0) config.max_tokens = mt;
|
||||
|
||||
await saveModelMutation.mutateAsync(config);
|
||||
toast.success(t("hermes.model.saveSuccess"));
|
||||
} catch (error) {
|
||||
toast.error(t("hermes.model.saveFailed"), {
|
||||
description: extractErrorMessage(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("hermes.model.description")}
|
||||
</p>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card p-5 mb-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2 sm:col-span-2">
|
||||
<Label htmlFor="hermes-model-default">
|
||||
{t("hermes.model.default", { defaultValue: "Default Model" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-model-default"
|
||||
value={defaultModel}
|
||||
onChange={(e) => setDefaultModel(e.target.value)}
|
||||
placeholder="anthropic/claude-opus-4-7"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.model.defaultHint", {
|
||||
defaultValue:
|
||||
"The default model to use, e.g. anthropic/claude-opus-4-7",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-model-provider">
|
||||
{t("hermes.model.provider", { defaultValue: "Provider" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-model-provider"
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
placeholder="openrouter"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.model.providerHint", {
|
||||
defaultValue:
|
||||
"Provider name for model routing (e.g. openrouter, anthropic)",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-model-baseurl">
|
||||
{t("hermes.model.baseUrl", { defaultValue: "Base URL" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-model-baseurl"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hermes.model.baseUrlHint", {
|
||||
defaultValue: "Override the API endpoint URL for this model",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-model-context">
|
||||
{t("hermes.model.contextLength", {
|
||||
defaultValue: "Context Length",
|
||||
})}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-model-context"
|
||||
type="number"
|
||||
value={contextLength}
|
||||
onChange={(e) => setContextLength(e.target.value)}
|
||||
placeholder="200000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hermes-model-maxtokens">
|
||||
{t("hermes.model.maxTokens", { defaultValue: "Max Tokens" })}
|
||||
</Label>
|
||||
<Input
|
||||
id="hermes-model-maxtokens"
|
||||
type="number"
|
||||
value={maxTokens}
|
||||
onChange={(e) => setMaxTokens(e.target.value)}
|
||||
placeholder="16384"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={saveModelMutation.isPending}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{saveModelMutation.isPending ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelPanel;
|
||||
Reference in New Issue
Block a user