claw-web-v2 / client /src /components /SettingsPanel.tsx
Claw Web
HARDCODE DeepInfra naglukho: fix model name, Dockerfile, remove API settings from UI
ce2c2fb
import { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { trpc } from "@/lib/trpc";
import {
X,
Save,
Key,
Cpu,
Sliders,
Shield,
Server,
DollarSign,
FileText,
Loader2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
const TABS = [
{ id: "model", label: "Model", icon: Cpu },
{ id: "params", label: "Parameters", icon: Sliders },
{ id: "permissions", label: "Permissions", icon: Shield },
{ id: "mcp", label: "MCP", icon: Server },
{ id: "costs", label: "Costs", icon: DollarSign },
{ id: "memory", label: "Memory", icon: FileText },
];
// Full parity with original claw-code (19 core + 18 extended + 4 MCP)
const TOOL_LIST = [
// Core 19 tools (original names)
"bash", "PowerShell", "read_file", "write_file", "edit_file",
"glob_search", "grep_search", "NotebookEdit",
"WebSearch", "WebFetch", "TodoWrite",
"Agent", "SendUserMessage", "Brief", "TestingPermission", "ToolSearch", "Config",
"Skill", "Sleep", "REPL", "StructuredOutput",
// Extended tools (full parity)
"TaskCreate", "TaskGet", "TaskList", "TaskOutput", "TaskStop", "TaskUpdate",
"CronCreate", "CronDelete", "CronList",
"LSP", "EnterPlanMode", "ExitPlanMode",
"EnterWorktree", "ExitWorktree",
"TeamCreate", "TeamDelete",
"RemoteTrigger", "SyntheticOutput",
// MCP
"mcp_tool", "list_mcp_resources", "read_mcp_resource", "mcp_auth",
];
interface SettingsPanelProps {
open: boolean;
onClose: () => void;
}
export function SettingsPanel({ open, onClose }: SettingsPanelProps) {
const [tab, setTab] = useState("model");
const utils = trpc.useUtils();
const { data: settings, isLoading } = trpc.settings.get.useQuery(undefined, {
enabled: open,
});
const updateSettings = trpc.settings.update.useMutation({
onSuccess: () => {
utils.settings.get.invalidate();
toast.success("Settings saved");
},
});
const { data: permissions } = trpc.permissions.list.useQuery(undefined, {
enabled: open && tab === "permissions",
});
const updatePermission = trpc.permissions.update.useMutation({
onSuccess: () => utils.permissions.list.invalidate(),
});
const { data: mcpServers } = trpc.mcp.list.useQuery(undefined, {
enabled: open && tab === "mcp",
});
const addMcp = trpc.mcp.add.useMutation({
onSuccess: () => utils.mcp.list.invalidate(),
});
const deleteMcp = trpc.mcp.delete.useMutation({
onSuccess: () => utils.mcp.list.invalidate(),
});
const { data: costSummary } = trpc.costs.summary.useQuery(undefined, {
enabled: open && tab === "costs",
});
// Local state for form
const [formState, setFormState] = useState<Record<string, any>>({});
useEffect(() => {
if (settings) {
setFormState({
model: settings.model || "",
apiProvider: settings.apiProvider || "default",
apiKey: settings.apiKey || "",
apiBaseUrl: settings.apiBaseUrl || "",
maxTokens: settings.maxTokens || 32768,
temperature: settings.temperature ?? 0.5,
topP: settings.topP ?? 0.95,
systemPrompt: settings.systemPrompt || "",
memoryContent: settings.memoryContent || "",
expandToolCalls: settings.expandToolCalls ?? 1,
autoApproveSafeTools: settings.autoApproveSafeTools ?? 1,
});
}
}, [settings]);
const saveField = (field: string, value: any) => {
updateSettings.mutate({ [field]: value });
};
// MCP form state
const [mcpName, setMcpName] = useState("");
const [mcpUrl, setMcpUrl] = useState("");
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
{/* Panel */}
<div className="relative ml-auto w-full max-w-lg bg-background border-l border-border flex flex-col h-full shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h2 className="text-sm font-semibold">Settings</h2>
<button
onClick={onClose}
className="size-7 rounded-md hover:bg-accent flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
>
<X className="size-4" />
</button>
</div>
{/* Tab navigation */}
<div className="flex border-b border-border overflow-x-auto">
{TABS.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={cn(
"flex items-center gap-1.5 px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors",
tab === t.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
<t.icon className="size-3.5" />
{t.label}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{isLoading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
</div>
)}
{/* Model tab */}
{tab === "model" && !isLoading && (
<div className="space-y-4">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
Model Name
</label>
<Input
value={formState.model || ""}
onChange={(e) =>
setFormState((s) => ({ ...s, model: e.target.value }))
}
placeholder="e.g. mimo, mimo-flash, qwen3-8b, llama-70b, deepseek"
className="font-mono text-sm"
/>
<p className="text-[10px] text-muted-foreground mt-1">
Use aliases (qwen-coder, deepseek, hermes, llama) or full model IDs. Default: Qwen3-Coder-480B-Turbo
</p>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
Quick Select
</label>
<div className="grid grid-cols-2 gap-1.5">
{[
{ id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo", label: "\u2B50 Qwen Coder 480B" },
{ id: "Qwen/Qwen3-235B-A22B-Instruct-2507", label: "Qwen3 235B" },
{ id: "deepseek-ai/DeepSeek-V3.2", label: "DeepSeek V3.2" },
{ id: "NousResearch/Hermes-3-Llama-3.1-70B", label: "\uD83D\uDD13 Hermes 70B" },
{ id: "stepfun-ai/Step-3.5-Flash", label: "Step 3.5 Flash" },
{ id: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B", label: "Nemotron 120B" },
{ id: "meta-llama/Llama-4-Maverick-17B-128E", label: "Llama 4 1M ctx" },
{ id: "Qwen/Qwen3.5-397B-A17B", label: "Qwen3.5 397B" },
].map((m) => (
<button
key={m.id}
onClick={() => {
setFormState((s) => ({ ...s, model: m.id }));
saveField("model", m.id);
}}
className={cn(
"text-xs px-2.5 py-1.5 rounded-md border transition-colors font-mono",
formState.model === m.id
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50"
)}
>
{m.label}
</button>
))}
</div>
</div>
<Button
size="sm"
onClick={() => saveField("model", formState.model)}
disabled={updateSettings.isPending}
>
<Save className="size-3.5 mr-1.5" />
Save Model
</Button>
{/* API: Hardcoded to DeepInfra — no user config needed */}
<div className="pt-3 border-t border-border">
<p className="text-[10px] text-muted-foreground">
API: DeepInfra (Qwen3-Coder-480B-Turbo) — hardcoded
</p>
</div>
</div>
)}
{/* Parameters tab */}
{tab === "params" && !isLoading && (
<div className="space-y-4">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
Max Tokens: {(formState.maxTokens || 16384).toLocaleString()}
</label>
<input
type="range"
min={1024}
max={32000}
step={1024}
value={formState.maxTokens || 16384}
onChange={(e) =>
setFormState((s) => ({
...s,
maxTokens: parseInt(e.target.value),
}))
}
className="w-full accent-primary"
/>
<div className="flex gap-1.5 mt-1.5">
{[4096, 8192, 16384, 24576, 32000].map((v) => (
<button
key={v}
onClick={() => {
setFormState((s) => ({ ...s, maxTokens: v }));
saveField("maxTokens", v);
}}
className={cn(
"text-[10px] px-1.5 py-0.5 rounded border transition-colors font-mono",
formState.maxTokens === v
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 text-muted-foreground"
)}
>
{v === 32000 ? '32k' : v >= 1024 ? `${v / 1024}k` : v}
</button>
))}
</div>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
Temperature: {formState.temperature?.toFixed(2)}
</label>
<input
type="range"
min={0}
max={2}
step={0.05}
value={formState.temperature || 0.7}
onChange={(e) =>
setFormState((s) => ({
...s,
temperature: parseFloat(e.target.value),
}))
}
className="w-full accent-primary"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
Top P: {formState.topP?.toFixed(2)}
</label>
<input
type="range"
min={0}
max={1}
step={0.05}
value={formState.topP || 1}
onChange={(e) =>
setFormState((s) => ({
...s,
topP: parseFloat(e.target.value),
}))
}
className="w-full accent-primary"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
System Prompt
</label>
<textarea
value={formState.systemPrompt || ""}
onChange={(e) =>
setFormState((s) => ({
...s,
systemPrompt: e.target.value,
}))
}
placeholder="Custom system prompt (leave empty for default)"
rows={4}
className="w-full bg-input border border-border rounded-md px-3 py-2 text-sm outline-none focus:border-primary resize-none font-mono"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-muted-foreground">
Auto-expand tool calls
</label>
<button
onClick={() => {
const val = formState.expandToolCalls ? 0 : 1;
setFormState((s) => ({ ...s, expandToolCalls: val }));
saveField("expandToolCalls", val);
}}
className={cn(
"w-10 h-5 rounded-full transition-colors relative",
formState.expandToolCalls
? "bg-primary"
: "bg-muted"
)}
>
<div
className={cn(
"size-4 rounded-full bg-white absolute top-0.5 transition-transform",
formState.expandToolCalls
? "translate-x-5"
: "translate-x-0.5"
)}
/>
</button>
</div>
<Button
size="sm"
onClick={() => {
saveField("maxTokens", formState.maxTokens);
saveField("temperature", formState.temperature);
saveField("topP", formState.topP);
if (formState.systemPrompt !== undefined) {
saveField("systemPrompt", formState.systemPrompt);
}
}}
disabled={updateSettings.isPending}
>
<Save className="size-3.5 mr-1.5" />
Save Parameters
</Button>
</div>
)}
{/* Permissions tab */}
{tab === "permissions" && (
<div className="space-y-2">
<p className="text-xs text-muted-foreground mb-3">
Configure which tools the agent can use and which require confirmation.
</p>
{TOOL_LIST.map((tool) => {
const perm = permissions?.find((p) => p.toolName === tool);
const allowed = perm ? perm.allowed : 1;
const requireConfirm = perm ? perm.requireConfirmation : 0;
return (
<div
key={tool}
className="flex items-center justify-between py-2 px-3 rounded-lg bg-secondary/30 border border-border"
>
<span className="font-mono text-xs">{tool}</span>
<div className="flex items-center gap-3">
<label className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
<input
type="checkbox"
checked={!!allowed}
onChange={(e) =>
updatePermission.mutate({
toolName: tool,
allowed: e.target.checked ? 1 : 0,
requireConfirmation: requireConfirm,
})
}
className="accent-primary"
/>
Allow
</label>
<label className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
<input
type="checkbox"
checked={!!requireConfirm}
onChange={(e) =>
updatePermission.mutate({
toolName: tool,
allowed: allowed,
requireConfirmation: e.target.checked ? 1 : 0,
})
}
className="accent-primary"
/>
Confirm
</label>
</div>
</div>
);
})}
</div>
)}
{/* MCP tab */}
{tab === "mcp" && (
<div className="space-y-4">
<p className="text-xs text-muted-foreground">
Connect MCP (Model Context Protocol) servers to extend agent capabilities.
</p>
{/* Add new MCP server */}
<div className="space-y-2 p-3 rounded-lg border border-border bg-secondary/20">
<Input
value={mcpName}
onChange={(e) => setMcpName(e.target.value)}
placeholder="Server name"
className="text-sm"
/>
<Input
value={mcpUrl}
onChange={(e) => setMcpUrl(e.target.value)}
placeholder="Server URL (e.g. http://localhost:3001/sse)"
className="text-sm font-mono"
/>
<Button
size="sm"
onClick={() => {
if (mcpName && mcpUrl) {
addMcp.mutate({ name: mcpName, url: mcpUrl });
setMcpName("");
setMcpUrl("");
}
}}
disabled={!mcpName || !mcpUrl}
>
Add Server
</Button>
</div>
{/* Server list */}
{mcpServers?.map((server) => (
<div
key={server.id}
className="flex items-center justify-between p-3 rounded-lg border border-border bg-secondary/30"
>
<div>
<div className="text-sm font-medium">{server.name}</div>
<div className="text-xs text-muted-foreground font-mono">
{server.url}
</div>
<div className="text-[10px] text-muted-foreground mt-0.5">
{server.transport} | {server.enabled ? "Enabled" : "Disabled"}
</div>
</div>
<button
onClick={() => deleteMcp.mutate({ id: server.id })}
className="text-destructive hover:text-destructive/80 transition-colors"
>
<X className="size-4" />
</button>
</div>
))}
{mcpServers?.length === 0 && (
<p className="text-xs text-muted-foreground text-center py-4">
No MCP servers configured
</p>
)}
</div>
)}
{/* Costs tab */}
{tab === "costs" && (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-3">
<div className="p-3 rounded-lg bg-secondary/30 border border-border text-center">
<div className="text-lg font-bold text-primary">
${costSummary?.totalCost?.toFixed(4) || "0.00"}
</div>
<div className="text-[10px] text-muted-foreground">Total Cost</div>
</div>
<div className="p-3 rounded-lg bg-secondary/30 border border-border text-center">
<div className="text-lg font-bold">
{(costSummary?.totalPromptTokens || 0).toLocaleString()}
</div>
<div className="text-[10px] text-muted-foreground">Prompt Tokens</div>
</div>
<div className="p-3 rounded-lg bg-secondary/30 border border-border text-center">
<div className="text-lg font-bold">
{(costSummary?.totalCompletionTokens || 0).toLocaleString()}
</div>
<div className="text-[10px] text-muted-foreground">
Completion Tokens
</div>
</div>
</div>
</div>
)}
{/* Memory tab */}
{tab === "memory" && !isLoading && (
<div className="space-y-4">
<p className="text-xs text-muted-foreground">
CLAW.md is persistent memory that persists across sessions. The agent
reads this at the start of every conversation.
</p>
<textarea
value={formState.memoryContent || ""}
onChange={(e) =>
setFormState((s) => ({
...s,
memoryContent: e.target.value,
}))
}
placeholder="# Project Notes\n\n- Key decisions\n- Preferences\n- Context"
rows={12}
className="w-full bg-input border border-border rounded-md px-3 py-2 text-sm outline-none focus:border-primary resize-none font-mono"
/>
<Button
size="sm"
onClick={() => saveField("memoryContent", formState.memoryContent)}
disabled={updateSettings.isPending}
>
<Save className="size-3.5 mr-1.5" />
Save Memory
</Button>
</div>
)}
</div>
</div>
</div>
);
}