fix(ui): resolve RightPanel JSX structure and close tag mismatch
- Fix the extra </> and )} that were't causing JSX parsing errors - Ensure proper nesting of motion.div and aside, and div elements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||||
import { useGatewayStore } from '../store/gatewayStore';
|
import { useGatewayStore } from '../store/gatewayStore';
|
||||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
import { toChatAgent, useChatStore } from '../store/chatStore';
|
||||||
@@ -6,6 +7,8 @@ import {
|
|||||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||||
MessageSquare, Cpu, FileText, User, Activity, FileCode
|
MessageSquare, Cpu, FileText, User, Activity, FileCode
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { cardHover, defaultTransition } from '../lib/animations';
|
||||||
|
import { Button, Badge, EmptyState } from './ui';
|
||||||
|
|
||||||
export function RightPanel() {
|
export function RightPanel() {
|
||||||
const {
|
const {
|
||||||
@@ -89,164 +92,205 @@ export function RightPanel() {
|
|||||||
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '系统时区';
|
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '系统时区';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-80 bg-white border-l border-gray-200 flex flex-col flex-shrink-0">
|
<aside className="w-80 bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 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="h-14 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between px-4 flex-shrink-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-1 text-gray-600">
|
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
|
||||||
<BarChart3 className="w-4 h-4" />
|
<BarChart3 className="w-4 h-4" />
|
||||||
<span className="font-medium">{topMetricValue}</span>
|
<span className="font-medium">{topMetricValue}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-400">{topMetricLabel}</span>
|
<span className="text-xs text-gray-500 dark:text-gray-400">{topMetricLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-gray-500">
|
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400" role="tablist">
|
||||||
<button
|
<Button
|
||||||
|
variant={activeTab === 'status' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('status')}
|
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'}`}
|
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||||
title="状态"
|
title="Status"
|
||||||
|
aria-label="Status"
|
||||||
|
aria-selected={activeTab === 'status'}
|
||||||
|
role="tab"
|
||||||
>
|
>
|
||||||
<Activity className="w-4 h-4" />
|
<Activity className="w-4 h-4" />
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={activeTab === 'files' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('files')}
|
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'}`}
|
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||||
title="文件"
|
title="Files"
|
||||||
|
aria-label="Files"
|
||||||
|
aria-selected={activeTab === 'files'}
|
||||||
|
role="tab"
|
||||||
>
|
>
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={activeTab === 'agent' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setActiveTab('agent')}
|
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'}`}
|
className="flex items-center gap-1 text-xs px-2 py-1"
|
||||||
title="Agent"
|
title="Agent"
|
||||||
|
aria-label="Agent"
|
||||||
|
aria-selected={activeTab === 'agent'}
|
||||||
|
role="tab"
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
||||||
{activeTab === 'agent' ? (
|
{activeTab === 'agent' ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||||
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center 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">
|
<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)}
|
{(selectedClone?.nickname || currentAgent?.name || 'Z').slice(0, 1)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-semibold text-gray-900">{selectedClone?.name || currentAgent?.name || 'ZCLAW'}</div>
|
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">{selectedClone?.name || currentAgent?.name || 'ZCLAW'}</div>
|
||||||
<div className="text-sm text-gray-500">{selectedClone?.role || 'AI coworker'}</div>
|
<div className="text-sm text-gray-500 dark:text-gray-400">{selectedClone?.role || 'AI coworker'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedClone ? (
|
{selectedClone ? (
|
||||||
isEditingAgent ? (
|
isEditingAgent ? (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={handleCancelEdit}
|
onClick={handleCancelEdit}
|
||||||
className="text-xs border border-gray-200 rounded-lg px-3 py-1.5 hover:bg-gray-50 transition-colors"
|
aria-label="Cancel edit"
|
||||||
>
|
>
|
||||||
取消
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
onClick={() => { handleSaveAgent().catch(() => {}); }}
|
onClick={() => { handleSaveAgent().catch(() => {}); }}
|
||||||
className="text-xs bg-orange-500 text-white rounded-lg px-3 py-1.5 hover:bg-orange-600 transition-colors"
|
aria-label="Save edit"
|
||||||
>
|
>
|
||||||
保存
|
Save
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={handleStartEdit}
|
onClick={handleStartEdit}
|
||||||
className="text-xs border border-gray-200 rounded-lg px-3 py-1.5 hover:bg-gray-50 transition-colors"
|
aria-label="Edit Agent"
|
||||||
>
|
>
|
||||||
编辑
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
<motion.div
|
||||||
<div className="text-sm font-semibold text-gray-900 mb-3">关于我</div>
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">About Me</div>
|
||||||
{isEditingAgent && agentDraft ? (
|
{isEditingAgent && agentDraft ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<AgentInput label="名称" value={agentDraft.name} onChange={(value) => setAgentDraft({ ...agentDraft, name: value })} />
|
<AgentInput label="Name" value={agentDraft.name} onChange={(value) => setAgentDraft({ ...agentDraft, name: value })} />
|
||||||
<AgentInput label="角色" value={agentDraft.role} onChange={(value) => setAgentDraft({ ...agentDraft, role: value })} />
|
<AgentInput label="Role" value={agentDraft.role} onChange={(value) => setAgentDraft({ ...agentDraft, role: value })} />
|
||||||
<AgentInput label="昵称" value={agentDraft.nickname} onChange={(value) => setAgentDraft({ ...agentDraft, nickname: value })} />
|
<AgentInput label="Nickname" value={agentDraft.nickname} onChange={(value) => setAgentDraft({ ...agentDraft, nickname: value })} />
|
||||||
<AgentInput label="模型" value={agentDraft.model} onChange={(value) => setAgentDraft({ ...agentDraft, model: value })} />
|
<AgentInput label="Model" value={agentDraft.model} onChange={(value) => setAgentDraft({ ...agentDraft, model: value })} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<AgentRow label="角色" value={selectedClone?.role || '-'} />
|
<AgentRow label="Role" value={selectedClone?.role || '-'} />
|
||||||
<AgentRow label="昵称" value={selectedClone?.nickname || '-'} />
|
<AgentRow label="Nickname" value={selectedClone?.nickname || '-'} />
|
||||||
<AgentRow label="模型" value={selectedClone?.model || currentModel} />
|
<AgentRow label="Model" value={selectedClone?.model || currentModel} />
|
||||||
<AgentRow label="Emoji" value={selectedClone?.nickname?.slice(0, 1) || '🦞'} />
|
<AgentRow label="Emoji" value={selectedClone?.nickname?.slice(0, 1) || '🦞'} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
<motion.div
|
||||||
<div className="text-sm font-semibold text-gray-900 mb-3">我眼中的你</div>
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">You in My Eyes</div>
|
||||||
{isEditingAgent && agentDraft ? (
|
{isEditingAgent && agentDraft ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<AgentInput label="名字" value={agentDraft.userName} onChange={(value) => setAgentDraft({ ...agentDraft, userName: value })} />
|
<AgentInput label="Name" value={agentDraft.userName} onChange={(value) => setAgentDraft({ ...agentDraft, userName: value })} />
|
||||||
<AgentInput label="角色" value={agentDraft.userRole} onChange={(value) => setAgentDraft({ ...agentDraft, userRole: value })} />
|
<AgentInput label="Role" 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="Scenarios" value={agentDraft.scenarios} onChange={(value) => setAgentDraft({ ...agentDraft, scenarios: value })} placeholder="coding, research" />
|
||||||
<AgentInput label="工作目录" value={agentDraft.workspaceDir} onChange={(value) => setAgentDraft({ ...agentDraft, workspaceDir: value })} />
|
<AgentInput label="Workspace" value={agentDraft.workspaceDir} onChange={(value) => setAgentDraft({ ...agentDraft, workspaceDir: value })} />
|
||||||
<AgentToggle label="文件限制" checked={agentDraft.restrictFiles} onChange={(value) => setAgentDraft({ ...agentDraft, restrictFiles: value })} />
|
<AgentToggle label="File Restriction" checked={agentDraft.restrictFiles} onChange={(value) => setAgentDraft({ ...agentDraft, restrictFiles: value })} />
|
||||||
<AgentToggle label="优化计划" checked={agentDraft.privacyOptIn} onChange={(value) => setAgentDraft({ ...agentDraft, privacyOptIn: value })} />
|
<AgentToggle label="Opt-in Program" checked={agentDraft.privacyOptIn} onChange={(value) => setAgentDraft({ ...agentDraft, privacyOptIn: value })} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<AgentRow label="名字" value={userNameDisplay} />
|
<AgentRow label="Name" value={userNameDisplay} />
|
||||||
<AgentRow label="称呼" value={userAddressing} />
|
<AgentRow label="Addressing" value={userAddressing} />
|
||||||
<AgentRow label="时区" value={localTimezone} />
|
<AgentRow label="Timezone" value={localTimezone} />
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="w-16 text-gray-400">专注于</div>
|
<div className="w-16 text-gray-500 dark:text-gray-400">Focus</div>
|
||||||
<div className="flex-1 flex flex-wrap gap-2">
|
<div className="flex-1 flex flex-wrap gap-2">
|
||||||
{focusAreas.map((item) => (
|
{focusAreas.map((item) => (
|
||||||
<span key={item} className="px-2 py-1 rounded-full bg-gray-100 text-xs text-gray-600">{item}</span>
|
<Badge key={item} variant="default">{item}</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AgentRow label="工作目录" value={selectedClone?.workspaceDir || workspaceInfo?.path || '~/.openfang/zclaw-workspace'} />
|
<AgentRow label="Workspace" value={selectedClone?.workspaceDir || workspaceInfo?.path || '~/.openfang/zclaw-workspace'} />
|
||||||
<AgentRow label="解析目录" value={selectedClone?.workspaceResolvedPath || workspaceInfo?.resolvedPath || '-'} />
|
<AgentRow label="Resolved" value={selectedClone?.workspaceResolvedPath || workspaceInfo?.resolvedPath || '-'} />
|
||||||
<AgentRow label="文件限制" value={selectedClone?.restrictFiles ? '已开启' : '未开启'} />
|
<AgentRow label="File Restriction" value={selectedClone?.restrictFiles ? 'Enabled' : 'Disabled'} />
|
||||||
<AgentRow label="优化计划" value={selectedClone?.privacyOptIn ? '已加入' : '未加入'} />
|
<AgentRow label="Opt-in" value={selectedClone?.privacyOptIn ? 'Joined' : 'Not joined'} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="text-sm font-semibold text-gray-900">Bootstrap 文件</div>
|
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100">Bootstrap Files</div>
|
||||||
<span className={`text-xs ${selectedClone?.bootstrapReady ? 'text-green-600' : 'text-gray-400'}`}>
|
<Badge variant={selectedClone?.bootstrapReady ? 'success' : 'default'}>
|
||||||
{selectedClone?.bootstrapReady ? '已生成' : '未生成'}
|
{selectedClone?.bootstrapReady ? 'Generated' : 'Not generated'}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
{bootstrapFiles.length > 0 ? bootstrapFiles.map((file) => (
|
{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 key={file.name} className="rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 px-3 py-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="font-medium text-gray-800">{file.name}</span>
|
<span className="font-medium text-gray-800 dark:text-gray-200">{file.name}</span>
|
||||||
<span className={`text-xs ${file.exists ? 'text-green-600' : 'text-red-500'}`}>
|
<Badge variant={file.exists ? 'success' : 'error'}>
|
||||||
{file.exists ? '存在' : '缺失'}
|
{file.exists ? 'Exists' : 'Missing'}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500 break-all">{file.path}</div>
|
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400 break-all">{file.path}</div>
|
||||||
</div>
|
</div>
|
||||||
)) : (
|
)) : (
|
||||||
<p className="text-sm text-gray-400">当前 Agent 尚未生成 bootstrap 文件。</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">No bootstrap files generated for this Agent.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === 'files' ? (
|
) : activeTab === 'files' ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 对话输出文件 */}
|
{/* 对话输出文件 */}
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||||
<FileCode className="w-4 h-4" />
|
<FileCode className="w-4 h-4" />
|
||||||
对话输出文件
|
对话输出文件
|
||||||
</h3>
|
</h3>
|
||||||
@@ -258,18 +302,18 @@ export function RightPanel() {
|
|||||||
{msg.files!.map((file, fileIdx) => (
|
{msg.files!.map((file, fileIdx) => (
|
||||||
<div
|
<div
|
||||||
key={`${msgIdx}-${fileIdx}`}
|
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"
|
className="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||||
title={file.path || file.name}
|
title={file.path || file.name}
|
||||||
>
|
>
|
||||||
<FileText className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
<FileText className="w-4 h-4 text-gray-500 dark:text-gray-400 flex-shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-gray-700 truncate">{file.name}</div>
|
<div className="text-gray-700 dark:text-gray-200 truncate">{file.name}</div>
|
||||||
{file.path && (
|
{file.path && (
|
||||||
<div className="text-xs text-gray-400 truncate">{file.path}</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{file.path}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{file.size && (
|
{file.size && (
|
||||||
<span className="text-xs text-gray-400 flex-shrink-0">
|
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
|
||||||
{file.size < 1024 ? `${file.size} B` :
|
{file.size < 1024 ? `${file.size} B` :
|
||||||
file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(1)} KB` :
|
file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(1)} KB` :
|
||||||
`${(file.size / (1024 * 1024)).toFixed(1)} MB`}
|
`${(file.size / (1024 * 1024)).toFixed(1)} MB`}
|
||||||
@@ -281,18 +325,23 @@ export function RightPanel() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8">
|
<EmptyState
|
||||||
<FileCode className="w-12 h-12 text-gray-200 mx-auto mb-3" />
|
icon={<FileCode className="w-8 h-8" />}
|
||||||
<p className="text-sm text-gray-400">对话中暂无输出文件</p>
|
title="No Output Files"
|
||||||
<p className="text-xs text-gray-300 mt-1">文件将在 AI 工具调用时显示</p>
|
description="Files will appear here when AI uses tools"
|
||||||
</div>
|
className="py-4"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 代码块 */}
|
{/* 代码块 */}
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-900">代码片段</h3>
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">代码片段</h3>
|
||||||
</div>
|
</div>
|
||||||
{messages.filter(m => m.codeBlocks && m.codeBlocks.length > 0).length > 0 ? (
|
{messages.filter(m => m.codeBlocks && m.codeBlocks.length > 0).length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -300,15 +349,13 @@ export function RightPanel() {
|
|||||||
msg.codeBlocks!.map((block, blockIdx) => (
|
msg.codeBlocks!.map((block, blockIdx) => (
|
||||||
<div
|
<div
|
||||||
key={`${msgIdx}-${blockIdx}`}
|
key={`${msgIdx}-${blockIdx}`}
|
||||||
className="px-3 py-2 bg-gray-50 rounded-lg text-sm"
|
className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-sm"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<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">
|
<Badge variant="default">{block.language || 'code'}</Badge>
|
||||||
{block.language || 'code'}
|
<span className="text-gray-700 dark:text-gray-200 truncate">{block.filename || 'Untitled'}</span>
|
||||||
</span>
|
|
||||||
<span className="text-gray-700 truncate">{block.filename || '未命名'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<pre className="text-xs text-gray-500 overflow-x-auto max-h-20">
|
<pre className="text-xs text-gray-500 dark:text-gray-400 overflow-x-auto max-h-20">
|
||||||
{block.content?.slice(0, 200)}{block.content && block.content.length > 200 ? '...' : ''}
|
{block.content?.slice(0, 200)}{block.content && block.content.length > 200 ? '...' : ''}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -316,33 +363,40 @@ export function RightPanel() {
|
|||||||
).slice(0, 5)}
|
).slice(0, 5)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-400 text-center py-4">对话中暂无代码片段</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">No code snippets in conversation</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Gateway 连接状态 */}
|
{/* Gateway 连接状态 */}
|
||||||
<div className={`rounded-lg border p-3 ${connected ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'}`}>
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
className={`rounded-lg border p-3 ${connected ? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' : 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700'}`}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{connected ? (
|
{connected ? (
|
||||||
<Wifi className="w-4 h-4 text-green-600" />
|
<Wifi className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
) : (
|
) : (
|
||||||
<WifiOff className="w-4 h-4 text-gray-400" />
|
<WifiOff className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||||
)}
|
)}
|
||||||
<span className={`text-xs font-semibold ${connected ? 'text-green-700' : 'text-gray-600'}`}>
|
<Badge variant={connected ? 'success' : 'default'}>
|
||||||
Gateway {connected ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接'}
|
Gateway {connected ? 'Connected' : connectionState === 'connecting' ? 'Connecting...' : connectionState === 'reconnecting' ? 'Reconnecting...' : 'Disconnected'}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{connected && (
|
{connected && (
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={() => { loadUsageStats(); loadPluginStatus(); loadClones(); }}
|
onClick={() => { loadUsageStats(); loadPluginStatus(); loadClones(); }}
|
||||||
className="p-1 text-gray-400 hover:text-orange-500 rounded transition-colors"
|
className="p-1 text-gray-500 hover:text-orange-500"
|
||||||
title="刷新数据"
|
title="Refresh data"
|
||||||
|
aria-label="Refresh data"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-3.5 h-3.5" />
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 text-xs">
|
<div className="space-y-1 text-xs">
|
||||||
@@ -362,20 +416,28 @@ export function RightPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!connected && connectionState !== 'connecting' && (
|
{!connected && connectionState !== 'connecting' && (
|
||||||
<button
|
<div className="mt-2">
|
||||||
onClick={handleReconnect}
|
<Button
|
||||||
className="mt-2 w-full text-xs bg-orange-500 text-white rounded py-1.5 hover:bg-orange-600 transition-colors"
|
variant="primary"
|
||||||
>
|
size="sm"
|
||||||
连接 Gateway
|
onClick={handleReconnect}
|
||||||
</button>
|
className="w-full"
|
||||||
|
>
|
||||||
|
Connect Gateway
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<p className="mt-2 text-xs text-red-500 truncate" title={error}>{error}</p>
|
<p className="mt-2 text-xs text-red-500 truncate" title={error}>{error}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 当前会话 */}
|
{/* 当前会话 */}
|
||||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
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">
|
<h3 className="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">
|
||||||
<MessageSquare className="w-3.5 h-3.5" />
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
当前会话
|
当前会话
|
||||||
@@ -398,10 +460,14 @@ export function RightPanel() {
|
|||||||
<span className="font-medium text-orange-600">{messages.length}</span>
|
<span className="font-medium text-orange-600">{messages.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 分身 */}
|
{/* 分身 */}
|
||||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
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">
|
<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" />
|
<Bot className="w-3.5 h-3.5" />
|
||||||
分身状态
|
分身状态
|
||||||
@@ -417,19 +483,23 @@ export function RightPanel() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{clones.length > 5 && (
|
{clones.length > 5 && (
|
||||||
<p className="text-xs text-gray-400">+{clones.length - 5} 个分身</p>
|
<p className="text-xs text-gray-500">+{clones.length - 5} 个分身</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-500">
|
||||||
{connected ? '暂无分身,在左侧栏创建' : '连接后可用'}
|
{connected ? '暂无分身,在左侧栏创建' : '连接后可用'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 用量统计 */}
|
{/* 用量统计 */}
|
||||||
{usageStats && (
|
{usageStats && (
|
||||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
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">
|
<h3 className="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">
|
||||||
<BarChart3 className="w-3.5 h-3.5" />
|
<BarChart3 className="w-3.5 h-3.5" />
|
||||||
用量统计
|
用量统计
|
||||||
@@ -448,12 +518,16 @@ export function RightPanel() {
|
|||||||
<span className="font-medium text-gray-900">{usageStats.totalTokens.toLocaleString()}</span>
|
<span className="font-medium text-gray-900">{usageStats.totalTokens.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 插件状态 */}
|
{/* 插件状态 */}
|
||||||
{pluginStatus.length > 0 && (
|
{pluginStatus.length > 0 && (
|
||||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
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">
|
<h3 className="text-xs font-semibold text-gray-700 mb-2 flex items-center gap-1.5">
|
||||||
<Plug className="w-3.5 h-3.5" />
|
<Plug className="w-3.5 h-3.5" />
|
||||||
插件 ({pluginStatus.length})
|
插件 ({pluginStatus.length})
|
||||||
@@ -462,17 +536,21 @@ export function RightPanel() {
|
|||||||
{pluginStatus.map((p: any, i: number) => (
|
{pluginStatus.map((p: any, i: number) => (
|
||||||
<div key={i} className="flex justify-between">
|
<div key={i} className="flex justify-between">
|
||||||
<span className="text-gray-600 truncate">{p.name || p.id}</span>
|
<span className="text-gray-600 truncate">{p.name || p.id}</span>
|
||||||
<span className={p.status === 'active' ? 'text-green-600' : 'text-gray-400'}>
|
<span className={p.status === 'active' ? 'text-green-600' : 'text-gray-500'}>
|
||||||
{p.status === 'active' ? '运行中' : '已停止'}
|
{p.status === 'active' ? '运行中' : '已停止'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 系统信息 */}
|
{/* 系统信息 */}
|
||||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3">
|
<motion.div
|
||||||
|
whileHover={cardHover}
|
||||||
|
transition={defaultTransition}
|
||||||
|
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">
|
<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" />
|
<Cpu className="w-3.5 h-3.5" />
|
||||||
运行概览
|
运行概览
|
||||||
@@ -495,9 +573,7 @@ export function RightPanel() {
|
|||||||
<span className="text-gray-700">{pluginStatus.length}</span>
|
<span className="text-gray-700">{pluginStatus.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
@@ -506,7 +582,7 @@ export function RightPanel() {
|
|||||||
function AgentRow({ label, value }: { label: string; value: string }) {
|
function AgentRow({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="w-16 text-gray-400">{label}</div>
|
<div className="w-16 text-gray-500">{label}</div>
|
||||||
<div className="flex-1 text-gray-700 break-all">{value}</div>
|
<div className="flex-1 text-gray-700 break-all">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -567,7 +643,7 @@ function AgentInput({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="text-xs text-gray-400 mb-1">{label}</div>
|
<div className="text-xs text-gray-500 mb-1">{label}</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
Reference in New Issue
Block a user