Spaces:
Sleeping
Sleeping
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> | |
| ); | |
| } | |