feat(hands): restructure Hands UI with Chinese localization
Major changes: - Add HandList.tsx component for left sidebar - Add HandTaskPanel.tsx for middle content area - Restructure Sidebar tabs: 分身/HANDS/Workflow - Remove Hands tab from RightPanel - Localize all UI text to Chinese - Archive legacy OpenClaw documentation - Add Hands integration lessons document - Update feature checklist with new components UI improvements: - Left sidebar now shows Hands list with status icons - Middle area shows selected Hand's tasks and results - Consistent styling with Tailwind CSS - Chinese status labels and buttons Documentation: - Create docs/archive/openclaw-legacy/ for old docs - Add docs/knowledge-base/hands-integration-lessons.md - Update docs/knowledge-base/feature-checklist.md - Update docs/knowledge-base/README.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { useChatStore } from '../store/chatStore';
|
||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, Activity,
|
||||
MessageSquare, Cpu, FileText, User, Activity, FileCode
|
||||
} from 'lucide-react';
|
||||
|
||||
export function RightPanel() {
|
||||
const {
|
||||
connectionState, gatewayVersion, error, clones, usageStats, pluginStatus,
|
||||
connect, loadClones, loadUsageStats, loadPluginStatus,
|
||||
connect, loadClones, loadUsageStats, loadPluginStatus, workspaceInfo, quickConfig, updateClone,
|
||||
} = useGatewayStore();
|
||||
const { messages, currentModel } = useChatStore();
|
||||
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent'>('status');
|
||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
|
||||
const connected = connectionState === 'connected';
|
||||
const selectedClone = useMemo(
|
||||
() => clones.find((clone) => clone.id === currentAgent?.id),
|
||||
[clones, currentAgent?.id]
|
||||
);
|
||||
const focusAreas = selectedClone?.scenarios?.length ? selectedClone.scenarios : ['coding', 'research'];
|
||||
const bootstrapFiles = selectedClone?.bootstrapFiles || [];
|
||||
const gatewayUrl = quickConfig.gatewayUrl || getStoredGatewayUrl();
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedClone || isEditingAgent) return;
|
||||
setAgentDraft(createAgentDraft(selectedClone, currentModel));
|
||||
}, [selectedClone, currentModel, isEditingAgent]);
|
||||
|
||||
// Load data when connected
|
||||
useEffect(() => {
|
||||
@@ -28,46 +44,311 @@ export function RightPanel() {
|
||||
connect().catch(() => {});
|
||||
};
|
||||
|
||||
const handleStartEdit = () => {
|
||||
if (!selectedClone) return;
|
||||
setAgentDraft(createAgentDraft(selectedClone, currentModel));
|
||||
setIsEditingAgent(true);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
if (selectedClone) {
|
||||
setAgentDraft(createAgentDraft(selectedClone, currentModel));
|
||||
}
|
||||
setIsEditingAgent(false);
|
||||
};
|
||||
|
||||
const handleSaveAgent = async () => {
|
||||
if (!selectedClone || !agentDraft || !agentDraft.name.trim()) return;
|
||||
const updatedClone = await updateClone(selectedClone.id, {
|
||||
name: agentDraft.name.trim(),
|
||||
role: agentDraft.role.trim() || undefined,
|
||||
nickname: agentDraft.nickname.trim() || undefined,
|
||||
model: agentDraft.model.trim() || undefined,
|
||||
scenarios: agentDraft.scenarios.split(',').map((item) => item.trim()).filter(Boolean),
|
||||
workspaceDir: agentDraft.workspaceDir.trim() || undefined,
|
||||
userName: agentDraft.userName.trim() || undefined,
|
||||
userRole: agentDraft.userRole.trim() || undefined,
|
||||
restrictFiles: agentDraft.restrictFiles,
|
||||
privacyOptIn: agentDraft.privacyOptIn,
|
||||
});
|
||||
if (updatedClone) {
|
||||
setCurrentAgent(toChatAgent(updatedClone));
|
||||
setAgentDraft(createAgentDraft(updatedClone, updatedClone.model || currentModel));
|
||||
setIsEditingAgent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const userMsgCount = messages.filter(m => m.role === 'user').length;
|
||||
const assistantMsgCount = messages.filter(m => m.role === 'assistant').length;
|
||||
const toolCallCount = messages.filter(m => m.role === 'tool').length;
|
||||
const topMetricValue = usageStats ? usageStats.totalTokens.toLocaleString() : messages.length.toString();
|
||||
const topMetricLabel = usageStats ? '累计 Token' : '当前消息';
|
||||
const runtimeSummary = connected ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接';
|
||||
const userNameDisplay = selectedClone?.userName || quickConfig.userName || '未设置';
|
||||
const userAddressing = selectedClone?.nickname || selectedClone?.userName || quickConfig.userName || '未设置';
|
||||
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '系统时区';
|
||||
|
||||
return (
|
||||
<aside className="w-72 bg-white border-l border-gray-200 flex flex-col flex-shrink-0">
|
||||
{/* 顶部 */}
|
||||
<aside className="w-80 bg-white border-l border-gray-200 flex flex-col flex-shrink-0">
|
||||
{/* 顶部工具栏 */}
|
||||
<div className="h-14 border-b border-gray-100 flex items-center justify-between px-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium text-gray-700 text-sm">系统状态</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1 text-gray-600">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
<span className="font-medium">{topMetricValue}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400">{topMetricLabel}</span>
|
||||
</div>
|
||||
{connected && (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<button
|
||||
onClick={() => { loadUsageStats(); loadPluginStatus(); loadClones(); }}
|
||||
className="p-1 text-gray-400 hover:text-orange-500 rounded transition-colors"
|
||||
title="刷新数据"
|
||||
onClick={() => setActiveTab('status')}
|
||||
className={`flex items-center gap-1 text-xs px-2 py-1 rounded ${activeTab === 'status' ? 'text-gray-900 bg-gray-100' : 'text-gray-400 hover:text-gray-700'}`}
|
||||
title="状态"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
<Activity className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setActiveTab('files')}
|
||||
className={`flex items-center gap-1 text-xs px-2 py-1 rounded ${activeTab === 'files' ? 'text-gray-900 bg-gray-100' : 'text-gray-400 hover:text-gray-700'}`}
|
||||
title="文件"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('agent')}
|
||||
className={`flex items-center gap-1 text-xs px-2 py-1 rounded ${activeTab === 'agent' ? 'text-gray-900 bg-gray-100' : 'text-gray-400 hover:text-gray-700'}`}
|
||||
title="Agent"
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
||||
{activeTab === 'agent' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 flex items-center justify-center text-white text-lg font-semibold">
|
||||
{(selectedClone?.nickname || currentAgent?.name || 'Z').slice(0, 1)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900">{selectedClone?.name || currentAgent?.name || 'ZCLAW'}</div>
|
||||
<div className="text-sm text-gray-500">{selectedClone?.role || 'AI coworker'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedClone ? (
|
||||
isEditingAgent ? (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
className="text-xs border border-gray-200 rounded-lg px-3 py-1.5 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { handleSaveAgent().catch(() => {}); }}
|
||||
className="text-xs bg-orange-500 text-white rounded-lg px-3 py-1.5 hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleStartEdit}
|
||||
className="text-xs border border-gray-200 rounded-lg px-3 py-1.5 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="text-sm font-semibold text-gray-900 mb-3">关于我</div>
|
||||
{isEditingAgent && agentDraft ? (
|
||||
<div className="space-y-2">
|
||||
<AgentInput label="名称" value={agentDraft.name} onChange={(value) => setAgentDraft({ ...agentDraft, name: value })} />
|
||||
<AgentInput label="角色" value={agentDraft.role} onChange={(value) => setAgentDraft({ ...agentDraft, role: value })} />
|
||||
<AgentInput label="昵称" value={agentDraft.nickname} onChange={(value) => setAgentDraft({ ...agentDraft, nickname: value })} />
|
||||
<AgentInput label="模型" value={agentDraft.model} onChange={(value) => setAgentDraft({ ...agentDraft, model: value })} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm">
|
||||
<AgentRow label="角色" value={selectedClone?.role || '-'} />
|
||||
<AgentRow label="昵称" value={selectedClone?.nickname || '-'} />
|
||||
<AgentRow label="模型" value={selectedClone?.model || currentModel} />
|
||||
<AgentRow label="Emoji" value={selectedClone?.nickname?.slice(0, 1) || '🦞'} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="text-sm font-semibold text-gray-900 mb-3">我眼中的你</div>
|
||||
{isEditingAgent && agentDraft ? (
|
||||
<div className="space-y-2">
|
||||
<AgentInput label="名字" value={agentDraft.userName} onChange={(value) => setAgentDraft({ ...agentDraft, userName: value })} />
|
||||
<AgentInput label="角色" value={agentDraft.userRole} onChange={(value) => setAgentDraft({ ...agentDraft, userRole: value })} />
|
||||
<AgentInput label="场景" value={agentDraft.scenarios} onChange={(value) => setAgentDraft({ ...agentDraft, scenarios: value })} placeholder="coding, research" />
|
||||
<AgentInput label="工作目录" value={agentDraft.workspaceDir} onChange={(value) => setAgentDraft({ ...agentDraft, workspaceDir: value })} />
|
||||
<AgentToggle label="文件限制" checked={agentDraft.restrictFiles} onChange={(value) => setAgentDraft({ ...agentDraft, restrictFiles: value })} />
|
||||
<AgentToggle label="优化计划" checked={agentDraft.privacyOptIn} onChange={(value) => setAgentDraft({ ...agentDraft, privacyOptIn: value })} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm">
|
||||
<AgentRow label="名字" value={userNameDisplay} />
|
||||
<AgentRow label="称呼" value={userAddressing} />
|
||||
<AgentRow label="时区" value={localTimezone} />
|
||||
<div className="flex gap-4">
|
||||
<div className="w-16 text-gray-400">专注于</div>
|
||||
<div className="flex-1 flex flex-wrap gap-2">
|
||||
{focusAreas.map((item) => (
|
||||
<span key={item} className="px-2 py-1 rounded-full bg-gray-100 text-xs text-gray-600">{item}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AgentRow label="工作目录" value={selectedClone?.workspaceDir || workspaceInfo?.path || '~/.openclaw/zclaw-workspace'} />
|
||||
<AgentRow label="解析目录" value={selectedClone?.workspaceResolvedPath || workspaceInfo?.resolvedPath || '-'} />
|
||||
<AgentRow label="文件限制" value={selectedClone?.restrictFiles ? '已开启' : '未开启'} />
|
||||
<AgentRow label="优化计划" value={selectedClone?.privacyOptIn ? '已加入' : '未加入'} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-gray-900">Bootstrap 文件</div>
|
||||
<span className={`text-xs ${selectedClone?.bootstrapReady ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
{selectedClone?.bootstrapReady ? '已生成' : '未生成'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
{bootstrapFiles.length > 0 ? bootstrapFiles.map((file) => (
|
||||
<div key={file.name} className="rounded-lg border border-gray-100 bg-gray-50 px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="font-medium text-gray-800">{file.name}</span>
|
||||
<span className={`text-xs ${file.exists ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{file.exists ? '存在' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 break-all">{file.path}</div>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-sm text-gray-400">当前 Agent 尚未生成 bootstrap 文件。</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : activeTab === 'files' ? (
|
||||
<div className="space-y-4">
|
||||
{/* 对话输出文件 */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
||||
<FileCode className="w-4 h-4" />
|
||||
对话输出文件
|
||||
</h3>
|
||||
</div>
|
||||
{messages.filter(m => m.files && m.files.length > 0).length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{messages.filter(m => m.files && m.files.length > 0).map((msg, msgIdx) => (
|
||||
<div key={msgIdx} className="space-y-1">
|
||||
{msg.files!.map((file, fileIdx) => (
|
||||
<div
|
||||
key={`${msgIdx}-${fileIdx}`}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-lg text-sm hover:bg-gray-100 cursor-pointer transition-colors"
|
||||
title={file.path || file.name}
|
||||
>
|
||||
<FileText className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-gray-700 truncate">{file.name}</div>
|
||||
{file.path && (
|
||||
<div className="text-xs text-gray-400 truncate">{file.path}</div>
|
||||
)}
|
||||
</div>
|
||||
{file.size && (
|
||||
<span className="text-xs text-gray-400 flex-shrink-0">
|
||||
{file.size < 1024 ? `${file.size} B` :
|
||||
file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(1)} KB` :
|
||||
`${(file.size / (1024 * 1024)).toFixed(1)} MB`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<FileCode className="w-12 h-12 text-gray-200 mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-400">对话中暂无输出文件</p>
|
||||
<p className="text-xs text-gray-300 mt-1">文件将在 AI 工具调用时显示</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 代码块 */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900">代码片段</h3>
|
||||
</div>
|
||||
{messages.filter(m => m.codeBlocks && m.codeBlocks.length > 0).length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{messages.filter(m => m.codeBlocks && m.codeBlocks.length > 0).flatMap((msg, msgIdx) =>
|
||||
msg.codeBlocks!.map((block, blockIdx) => (
|
||||
<div
|
||||
key={`${msgIdx}-${blockIdx}`}
|
||||
className="px-3 py-2 bg-gray-50 rounded-lg text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs px-1.5 py-0.5 bg-gray-200 rounded text-gray-600">
|
||||
{block.language || 'code'}
|
||||
</span>
|
||||
<span className="text-gray-700 truncate">{block.filename || '未命名'}</span>
|
||||
</div>
|
||||
<pre className="text-xs text-gray-500 overflow-x-auto max-h-20">
|
||||
{block.content?.slice(0, 200)}{block.content && block.content.length > 200 ? '...' : ''}
|
||||
</pre>
|
||||
</div>
|
||||
))
|
||||
).slice(0, 5)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400 text-center py-4">对话中暂无代码片段</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Gateway 连接状态 */}
|
||||
<div className={`rounded-lg border p-3 ${connected ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{connected ? (
|
||||
<Wifi className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<WifiOff className="w-4 h-4 text-gray-400" />
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{connected ? (
|
||||
<Wifi className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<WifiOff className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
<span className={`text-xs font-semibold ${connected ? 'text-green-700' : 'text-gray-600'}`}>
|
||||
Gateway {connected ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
{connected && (
|
||||
<button
|
||||
onClick={() => { loadUsageStats(); loadPluginStatus(); loadClones(); }}
|
||||
className="p-1 text-gray-400 hover:text-orange-500 rounded transition-colors"
|
||||
title="刷新数据"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<span className={`text-xs font-semibold ${connected ? 'text-green-700' : 'text-gray-600'}`}>
|
||||
Gateway {connected ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">地址</span>
|
||||
<span className="text-gray-700 font-mono">127.0.0.1:18789</span>
|
||||
<span className="text-gray-700 font-mono">{gatewayUrl}</span>
|
||||
</div>
|
||||
{gatewayVersion && (
|
||||
<div className="flex justify-between">
|
||||
@@ -123,7 +404,7 @@ export function RightPanel() {
|
||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
||||
<h3 className="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">
|
||||
<Bot className="w-3.5 h-3.5" />
|
||||
分身
|
||||
分身状态
|
||||
</h3>
|
||||
{clones.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
@@ -194,24 +475,123 @@ export function RightPanel() {
|
||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
||||
<h3 className="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">
|
||||
<Cpu className="w-3.5 h-3.5" />
|
||||
系统信息
|
||||
运行概览
|
||||
</h3>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">ZCLAW 版本</span>
|
||||
<span className="text-gray-700">v0.2.0</span>
|
||||
<span className="text-gray-500">连接状态</span>
|
||||
<span className="text-gray-700">{runtimeSummary}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">协议版本</span>
|
||||
<span className="text-gray-700">Gateway v3</span>
|
||||
<span className="text-gray-500">Gateway 版本</span>
|
||||
<span className="text-gray-700">{gatewayVersion || '-'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">平台</span>
|
||||
<span className="text-gray-700">Tauri 2.0</span>
|
||||
<span className="text-gray-500">已加载分身</span>
|
||||
<span className="text-gray-700">{clones.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">已加载插件</span>
|
||||
<span className="text-gray-700">{pluginStatus.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="w-16 text-gray-400">{label}</div>
|
||||
<div className="flex-1 text-gray-700 break-all">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type AgentDraft = {
|
||||
name: string;
|
||||
role: string;
|
||||
nickname: string;
|
||||
model: string;
|
||||
scenarios: string;
|
||||
workspaceDir: string;
|
||||
userName: string;
|
||||
userRole: string;
|
||||
restrictFiles: boolean;
|
||||
privacyOptIn: boolean;
|
||||
};
|
||||
|
||||
function createAgentDraft(
|
||||
clone: {
|
||||
name: string;
|
||||
role?: string;
|
||||
nickname?: string;
|
||||
model?: string;
|
||||
scenarios?: string[];
|
||||
workspaceDir?: string;
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
restrictFiles?: boolean;
|
||||
privacyOptIn?: boolean;
|
||||
},
|
||||
currentModel: string
|
||||
): AgentDraft {
|
||||
return {
|
||||
name: clone.name || '',
|
||||
role: clone.role || '',
|
||||
nickname: clone.nickname || '',
|
||||
model: clone.model || currentModel,
|
||||
scenarios: clone.scenarios?.join(', ') || '',
|
||||
workspaceDir: clone.workspaceDir || '~/.openclaw/zclaw-workspace',
|
||||
userName: clone.userName || '',
|
||||
userRole: clone.userRole || '',
|
||||
restrictFiles: clone.restrictFiles ?? true,
|
||||
privacyOptIn: clone.privacyOptIn ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function AgentInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<div className="text-xs text-gray-400 mb-1">{label}</div>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full text-sm border border-gray-200 rounded-lg px-3 py-2 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentToggle({
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex items-center justify-between text-sm text-gray-700 border border-gray-100 rounded-lg px-3 py-2">
|
||||
<span>{label}</span>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user