From 717f2eab4f3179248c1ca1cc7224886ea2e0d125 Mon Sep 17 00:00:00 2001
From: iven
Date: Sat, 11 Apr 2026 00:26:04 +0800
Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=8640=E4=B8=AA=E6=AD=BB?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=96=87=E4=BB=B6=20(~9,639=E8=A1=8C)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
删除无任何活跃渲染路径引用的组件:
- Automation/ 全目录 (7文件, 2,598行)
- WorkflowBuilder/ 全目录 (14文件, 1,539行)
- SchedulerPanel + 依赖树 (5文件, 2,595行)
- 独立死组件 (14文件, 2,907行)
含 SkillMarket, HandsPanel, ErrorNotification 等
- PipelineResultPreview 根目录副本 (534行, 活跃版在 pipeline/)
---
.../components/Automation/ApprovalQueue.tsx | 352 -------
.../components/Automation/AutomationCard.tsx | 402 -------
.../Automation/AutomationFilters.tsx | 204 ----
.../components/Automation/AutomationPanel.tsx | 653 ------------
.../components/Automation/BatchActionBar.tsx | 216 ----
.../components/Automation/ExecutionResult.tsx | 395 -------
.../components/Automation/ScheduleEditor.tsx | 378 -------
desktop/src/components/Automation/index.ts | 26 -
desktop/src/components/ChannelList.tsx | 132 ---
desktop/src/components/ConnectionStatus.tsx | 286 -----
desktop/src/components/ErrorNotification.tsx | 270 -----
desktop/src/components/HandList.tsx | 131 ---
desktop/src/components/HandTaskPanel.tsx | 326 ------
desktop/src/components/HandsPanel.tsx | 641 ------------
.../src/components/PipelineResultPreview.tsx | 534 ----------
desktop/src/components/PipelinesPanel.tsx | 567 ----------
.../src/components/SaaS/SubscriptionPanel.tsx | 173 ---
desktop/src/components/SchedulerPanel.tsx | 990 ------------------
desktop/src/components/SimpleTopBar.tsx | 54 -
.../src/components/SkillMarket/SkillCard.tsx | 211 ----
.../WorkflowBuilder/NodePalette.tsx | 92 --
.../WorkflowBuilder/PropertyPanel.tsx | 301 ------
.../WorkflowBuilder/WorkflowBuilder.tsx | 324 ------
.../WorkflowBuilder/WorkflowToolbar.tsx | 166 ---
.../src/components/WorkflowBuilder/index.ts | 21 -
.../WorkflowBuilder/nodes/ConditionNode.tsx | 81 --
.../WorkflowBuilder/nodes/ExportNode.tsx | 72 --
.../WorkflowBuilder/nodes/HandNode.tsx | 76 --
.../WorkflowBuilder/nodes/HttpNode.tsx | 81 --
.../WorkflowBuilder/nodes/InputNode.tsx | 54 -
.../WorkflowBuilder/nodes/LlmNode.tsx | 70 --
.../nodes/OrchestrationNode.tsx | 81 --
.../WorkflowBuilder/nodes/ParallelNode.tsx | 55 -
.../WorkflowBuilder/nodes/SkillNode.tsx | 65 --
desktop/src/components/WorkflowEditor.tsx | 479 ---------
desktop/src/components/WorkflowHistory.tsx | 281 -----
desktop/src/components/WorkflowList.tsx | 510 ---------
.../src/components/pipeline/IntentInput.tsx | 400 -------
desktop/src/components/ui/ErrorAlert.tsx | 345 ------
39 files changed, 10495 deletions(-)
delete mode 100644 desktop/src/components/Automation/ApprovalQueue.tsx
delete mode 100644 desktop/src/components/Automation/AutomationCard.tsx
delete mode 100644 desktop/src/components/Automation/AutomationFilters.tsx
delete mode 100644 desktop/src/components/Automation/AutomationPanel.tsx
delete mode 100644 desktop/src/components/Automation/BatchActionBar.tsx
delete mode 100644 desktop/src/components/Automation/ExecutionResult.tsx
delete mode 100644 desktop/src/components/Automation/ScheduleEditor.tsx
delete mode 100644 desktop/src/components/Automation/index.ts
delete mode 100644 desktop/src/components/ChannelList.tsx
delete mode 100644 desktop/src/components/ConnectionStatus.tsx
delete mode 100644 desktop/src/components/ErrorNotification.tsx
delete mode 100644 desktop/src/components/HandList.tsx
delete mode 100644 desktop/src/components/HandTaskPanel.tsx
delete mode 100644 desktop/src/components/HandsPanel.tsx
delete mode 100644 desktop/src/components/PipelineResultPreview.tsx
delete mode 100644 desktop/src/components/PipelinesPanel.tsx
delete mode 100644 desktop/src/components/SaaS/SubscriptionPanel.tsx
delete mode 100644 desktop/src/components/SchedulerPanel.tsx
delete mode 100644 desktop/src/components/SimpleTopBar.tsx
delete mode 100644 desktop/src/components/SkillMarket/SkillCard.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/NodePalette.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/PropertyPanel.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/WorkflowBuilder.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/WorkflowToolbar.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/index.ts
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/ConditionNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/ExportNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/HandNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/HttpNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/InputNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/LlmNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/OrchestrationNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/ParallelNode.tsx
delete mode 100644 desktop/src/components/WorkflowBuilder/nodes/SkillNode.tsx
delete mode 100644 desktop/src/components/WorkflowEditor.tsx
delete mode 100644 desktop/src/components/WorkflowHistory.tsx
delete mode 100644 desktop/src/components/WorkflowList.tsx
delete mode 100644 desktop/src/components/pipeline/IntentInput.tsx
delete mode 100644 desktop/src/components/ui/ErrorAlert.tsx
diff --git a/desktop/src/components/Automation/ApprovalQueue.tsx b/desktop/src/components/Automation/ApprovalQueue.tsx
deleted file mode 100644
index 5b8c9bd..0000000
--- a/desktop/src/components/Automation/ApprovalQueue.tsx
+++ /dev/null
@@ -1,352 +0,0 @@
-/**
- * ApprovalQueue - Approval Management Component
- *
- * Displays pending approvals for hand executions that require
- * human approval, with approve/reject actions.
- *
- * @module components/Automation/ApprovalQueue
- */
-
-import { useState, useEffect, useCallback } from 'react';
-import { useHandStore } from '../../store/handStore';
-import type { Approval, ApprovalStatus } from '../../store/handStore';
-import {
- Clock,
- CheckCircle,
- XCircle,
- AlertTriangle,
- RefreshCw,
-} from 'lucide-react';
-import { useToast } from '../ui/Toast';
-
-// === Status Config ===
-
-const STATUS_CONFIG: Record = {
- pending: {
- label: '待处理',
- className: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
- icon: Clock,
- },
- approved: {
- label: '已批准',
- className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
- icon: CheckCircle,
- },
- rejected: {
- label: '已拒绝',
- className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
- icon: XCircle,
- },
- expired: {
- label: '已过期',
- className: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
- icon: AlertTriangle,
- },
-};
-
-// === Component Props ===
-
-interface ApprovalQueueProps {
- showFilters?: boolean;
- maxHeight?: string;
- onApprove?: (approval: Approval) => void;
- onReject?: (approval: Approval) => void;
-}
-
-// === Approval Card Component ===
-
-interface ApprovalCardProps {
- approval: Approval;
- onApprove: () => Promise;
- onReject: (reason: string) => Promise;
- isProcessing: boolean;
-}
-
-function ApprovalCard({ approval, onApprove, onReject, isProcessing }: ApprovalCardProps) {
- const [showRejectInput, setShowRejectInput] = useState(false);
- const [rejectReason, setRejectReason] = useState('');
- const StatusIcon = STATUS_CONFIG[approval.status].icon;
-
- const handleReject = useCallback(async () => {
- if (!rejectReason.trim()) {
- setShowRejectInput(true);
- return;
- }
- await onReject(rejectReason);
- setShowRejectInput(false);
- setRejectReason('');
- }, [rejectReason, onReject]);
-
- const timeAgo = useCallback((dateStr: string) => {
- const date = new Date(dateStr);
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffMins = Math.floor(diffMs / 60000);
- const diffHours = Math.floor(diffMins / 60);
- const diffDays = Math.floor(diffHours / 24);
-
- if (diffMins < 1) return '刚刚';
- if (diffMins < 60) return `${diffMins} 分钟前`;
- if (diffHours < 24) return `${diffHours} 小时前`;
- return `${diffDays} 天前`;
- }, []);
-
- return (
-
- {/* Header */}
-
-
-
-
- {STATUS_CONFIG[approval.status].label}
-
-
- {timeAgo(approval.requestedAt)}
-
-
-
-
- {/* Content */}
-
-
- {approval.handName}
-
- {approval.reason && (
-
{approval.reason}
- )}
- {approval.action && (
-
- 操作: {approval.action}
-
- )}
-
-
- {/* Params Preview */}
- {approval.params && Object.keys(approval.params).length > 0 && (
-
-
参数:
-
- {JSON.stringify(approval.params, null, 2)}
-
-
- )}
-
- {/* Reject Input */}
- {showRejectInput && (
-
-
- )}
-
- {/* Actions */}
- {approval.status === 'pending' && (
-
-
-
-
- )}
-
- {/* Response Info */}
- {approval.status !== 'pending' && approval.respondedAt && (
-
- {approval.respondedBy && `由 ${approval.respondedBy} `}
- {STATUS_CONFIG[approval.status].label}
- {approval.responseReason && ` - ${approval.responseReason}`}
-
- )}
-
- );
-}
-
-// === Main Component ===
-
-export function ApprovalQueue({
- showFilters = true,
- maxHeight = '400px',
- onApprove,
- onReject,
-}: ApprovalQueueProps) {
- const { toast } = useToast();
-
- // Store state
- const approvals = useHandStore(s => s.approvals);
- const loadApprovals = useHandStore(s => s.loadApprovals);
- const respondToApproval = useHandStore(s => s.respondToApproval);
- const isLoading = useHandStore(s => s.isLoading);
-
- // Local state
- const [statusFilter, setStatusFilter] = useState('pending');
- const [processingIds, setProcessingIds] = useState>(new Set());
-
- // Load approvals on mount
- useEffect(() => {
- loadApprovals(statusFilter === 'all' ? undefined : statusFilter);
- }, [loadApprovals, statusFilter]);
-
- // Handle approve
- const handleApprove = useCallback(async (approval: Approval) => {
- setProcessingIds(prev => new Set(prev).add(approval.id));
- try {
- await respondToApproval(approval.id, true);
- toast(`已批准: ${approval.handName}`, 'success');
- onApprove?.(approval);
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err);
- toast(`批准失败: ${errorMsg}`, 'error');
- } finally {
- setProcessingIds(prev => {
- const next = new Set(prev);
- next.delete(approval.id);
- return next;
- });
- }
- }, [respondToApproval, toast, onApprove]);
-
- // Handle reject
- const handleReject = useCallback(async (approval: Approval, reason: string) => {
- setProcessingIds(prev => new Set(prev).add(approval.id));
- try {
- await respondToApproval(approval.id, false, reason);
- toast(`已拒绝: ${approval.handName}`, 'success');
- onReject?.(approval);
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err);
- toast(`拒绝失败: ${errorMsg}`, 'error');
- } finally {
- setProcessingIds(prev => {
- const next = new Set(prev);
- next.delete(approval.id);
- return next;
- });
- }
- }, [respondToApproval, toast, onReject]);
-
- // Filter approvals
- const filteredApprovals = statusFilter === 'all'
- ? approvals
- : approvals.filter(a => a.status === statusFilter);
-
- // Stats
- const stats = {
- pending: approvals.filter(a => a.status === 'pending').length,
- approved: approvals.filter(a => a.status === 'approved').length,
- rejected: approvals.filter(a => a.status === 'rejected').length,
- expired: approvals.filter(a => a.status === 'expired').length,
- };
-
- return (
-
- {/* Header */}
-
-
-
-
- 审批队列
-
- {stats.pending > 0 && (
-
- {stats.pending} 待处理
-
- )}
-
-
-
-
- {/* Filters */}
- {showFilters && (
-
- {[
- { value: 'pending', label: '待处理', count: stats.pending },
- { value: 'approved', label: '已批准', count: stats.approved },
- { value: 'rejected', label: '已拒绝', count: stats.rejected },
- { value: 'all', label: '全部', count: approvals.length },
- ].map(option => (
-
- ))}
-
- )}
-
- {/* Content */}
-
- {isLoading && approvals.length === 0 ? (
-
-
-
- ) : filteredApprovals.length === 0 ? (
-
-
-
- {statusFilter === 'pending' ? '暂无待处理的审批' : '暂无审批记录'}
-
-
- ) : (
-
- {filteredApprovals.map(approval => (
-
handleApprove(approval)}
- onReject={(reason) => handleReject(approval, reason)}
- isProcessing={processingIds.has(approval.id)}
- />
- ))}
-
- )}
-
-
- );
-}
-
-export default ApprovalQueue;
diff --git a/desktop/src/components/Automation/AutomationCard.tsx b/desktop/src/components/Automation/AutomationCard.tsx
deleted file mode 100644
index d969c2b..0000000
--- a/desktop/src/components/Automation/AutomationCard.tsx
+++ /dev/null
@@ -1,402 +0,0 @@
-/**
- * AutomationCard - Unified Card for Hands and Workflows
- *
- * Displays automation items with status, parameters, and actions.
- * Supports both grid and list view modes.
- *
- * @module components/Automation/AutomationCard
- */
-
-import { useState, useCallback } from 'react';
-import type { AutomationItem, AutomationStatus } from '../../types/automation';
-import { CATEGORY_CONFIGS } from '../../types/automation';
-import type { HandParameter } from '../../types/hands';
-import { HandParamsForm } from '../HandParamsForm';
-import {
- Zap,
- Clock,
- CheckCircle,
- XCircle,
- AlertTriangle,
- Loader2,
- Settings,
- Play,
- MoreVertical,
-} from 'lucide-react';
-
-// === Status Config ===
-
-const STATUS_CONFIG: Record = {
- idle: {
- label: '就绪',
- className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
- dotClass: 'bg-green-500',
- },
- running: {
- label: '运行中',
- className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
- dotClass: 'bg-blue-500 animate-pulse',
- icon: Loader2,
- },
- needs_approval: {
- label: '待审批',
- className: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
- dotClass: 'bg-yellow-500',
- icon: AlertTriangle,
- },
- error: {
- label: '错误',
- className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
- dotClass: 'bg-red-500',
- icon: XCircle,
- },
- unavailable: {
- label: '不可用',
- className: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
- dotClass: 'bg-gray-400',
- },
- setup_needed: {
- label: '需配置',
- className: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
- dotClass: 'bg-orange-500',
- icon: Settings,
- },
- completed: {
- label: '已完成',
- className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
- dotClass: 'bg-green-500',
- icon: CheckCircle,
- },
- paused: {
- label: '已暂停',
- className: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
- dotClass: 'bg-gray-400',
- },
-};
-
-// === Component Props ===
-
-interface AutomationCardProps {
- item: AutomationItem;
- viewMode?: 'grid' | 'list';
- isSelected?: boolean;
- isExecuting?: boolean;
- onSelect?: (selected: boolean) => void;
- onExecute?: (params?: Record) => void;
- onClick?: () => void;
-}
-
-// === Status Badge Component ===
-
-function StatusBadge({ status }: { status: AutomationStatus }) {
- const config = STATUS_CONFIG[status] || STATUS_CONFIG.unavailable;
- const Icon = config.icon;
-
- return (
-
- {Icon ? (
-
- ) : (
-
- )}
- {config.label}
-
- );
-}
-
-// === Type Badge Component ===
-
-function TypeBadge({ type }: { type: 'hand' | 'workflow' }) {
- const isHand = type === 'hand';
- return (
-
- {isHand ? '自主能力' : '工作流'}
-
- );
-}
-
-// === Category Badge Component ===
-
-function CategoryBadge({ category }: { category: string }) {
- const config = CATEGORY_CONFIGS[category as keyof typeof CATEGORY_CONFIGS];
- if (!config) return null;
-
- return (
-
- {config.label}
-
- );
-}
-
-// === Main Component ===
-
-export function AutomationCard({
- item,
- viewMode = 'grid',
- isSelected = false,
- isExecuting = false,
- onSelect,
- onExecute,
- onClick,
-}: AutomationCardProps) {
- const [showParams, setShowParams] = useState(false);
- const [paramValues, setParamValues] = useState>({});
- const [paramErrors, setParamErrors] = useState>({});
-
- const hasParameters = item.parameters && item.parameters.length > 0;
- const canActivate = item.status === 'idle' || item.status === 'setup_needed';
-
- // Initialize default parameter values
- const initializeDefaults = useCallback(() => {
- if (item.parameters) {
- const defaults: Record = {};
- item.parameters.forEach(p => {
- if (p.defaultValue !== undefined) {
- defaults[p.name] = p.defaultValue;
- }
- });
- setParamValues(defaults);
- }
- }, [item.parameters]);
-
- // Handle execute click
- const handleExecuteClick = useCallback(() => {
- if (hasParameters && !showParams) {
- initializeDefaults();
- setShowParams(true);
- return;
- }
-
- // Validate parameters
- if (showParams && item.parameters) {
- const errors: Record = {};
- item.parameters.forEach(param => {
- if (param.required) {
- const value = paramValues[param.name];
- if (value === undefined || value === null || value === '') {
- errors[param.name] = `${param.label} is required`;
- }
- }
- });
-
- if (Object.keys(errors).length > 0) {
- setParamErrors(errors);
- return;
- }
- }
-
- onExecute?.(showParams ? paramValues : undefined);
- setShowParams(false);
- setParamErrors({});
- }, [hasParameters, showParams, initializeDefaults, item.parameters, paramValues, onExecute]);
-
- // Handle checkbox change
- const handleCheckboxChange = useCallback((e: React.ChangeEvent) => {
- e.stopPropagation();
- onSelect?.(e.target.checked);
- }, [onSelect]);
-
- // Get icon for item
- const getItemIcon = () => {
- if (item.icon) {
- // Map string icon names to components
- const iconMap: Record = {
- Video: '🎬',
- UserPlus: '👤',
- Database: '🗄️',
- TrendingUp: '📈',
- Search: '🔍',
- Twitter: '🐦',
- Globe: '🌐',
- Zap: '⚡',
- };
- return iconMap[item.icon] || '🤖';
- }
- return item.type === 'hand' ? '🤖' : '📋';
- };
-
- if (viewMode === 'list') {
- return (
-
- {/* Checkbox */}
-
e.stopPropagation()}
- />
-
- {/* Icon */}
-
{getItemIcon()}
-
- {/* Info */}
-
-
-
{item.name}
-
-
-
{item.description}
-
-
- {/* Status */}
-
-
- {/* Actions */}
-
-
-
-
-
- );
- }
-
- // Grid view
- return (
-
- {/* Selection checkbox */}
-
- e.stopPropagation()}
- />
-
-
- {/* Content */}
-
- {/* Header */}
-
-
- {getItemIcon()}
-
{item.name}
-
-
-
-
- {/* Description */}
-
{item.description}
-
- {/* Meta */}
-
-
-
- {item.schedule?.enabled && (
-
-
- 已调度
-
- )}
-
-
- {/* Parameters Form (shown when activating) */}
- {showParams && item.parameters && item.parameters.length > 0 && (
-
-
-
- )}
-
- {/* Actions */}
-
-
-
-
-
- {/* Schedule indicator */}
- {item.schedule?.nextRun && (
-
- 下次运行: {new Date(item.schedule.nextRun).toLocaleString('zh-CN')}
-
- )}
-
-
- );
-}
-
-export default AutomationCard;
diff --git a/desktop/src/components/Automation/AutomationFilters.tsx b/desktop/src/components/Automation/AutomationFilters.tsx
deleted file mode 100644
index 12f520e..0000000
--- a/desktop/src/components/Automation/AutomationFilters.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
- * AutomationFilters - Category and Search Filters
- *
- * Provides category tabs, search input, and view mode toggle
- * for the automation panel.
- *
- * @module components/Automation/AutomationFilters
- */
-
-import { useState, useCallback } from 'react';
-import type { CategoryType, CategoryStats } from '../../types/automation';
-import { CATEGORY_CONFIGS } from '../../types/automation';
-import {
- Search,
- Grid,
- List,
- Layers,
- Database,
- MessageSquare,
- Video,
- TrendingUp,
- Zap,
- ChevronDown,
-} from 'lucide-react';
-
-// === Icon Map ===
-
-const CATEGORY_ICONS: Record = {
- all: Layers,
- research: Search,
- data: Database,
- automation: Zap,
- communication: MessageSquare,
- content: Video,
- productivity: TrendingUp,
-};
-
-// === Component Props ===
-
-interface AutomationFiltersProps {
- selectedCategory: CategoryType;
- onCategoryChange: (category: CategoryType) => void;
- searchQuery: string;
- onSearchChange: (query: string) => void;
- viewMode: 'grid' | 'list';
- onViewModeChange: (mode: 'grid' | 'list') => void;
- categoryStats: CategoryStats;
-}
-
-// === Main Component ===
-
-export function AutomationFilters({
- selectedCategory,
- onCategoryChange,
- searchQuery,
- onSearchChange,
- viewMode,
- onViewModeChange,
- categoryStats,
-}: AutomationFiltersProps) {
- const [showCategoryDropdown, setShowCategoryDropdown] = useState(false);
-
- // Handle search input
- const handleSearchChange = useCallback((e: React.ChangeEvent) => {
- onSearchChange(e.target.value);
- }, [onSearchChange]);
-
- // Handle category click
- const handleCategoryClick = useCallback((category: CategoryType) => {
- onCategoryChange(category);
- setShowCategoryDropdown(false);
- }, [onCategoryChange]);
-
- // Get categories with counts
- const categories = Object.entries(CATEGORY_CONFIGS).map(([key, config]) => ({
- ...config,
- count: categoryStats[key as CategoryType] || 0,
- }));
-
- // Selected category config
- const selectedConfig = CATEGORY_CONFIGS[selectedCategory];
-
- return (
-
- {/* Search and View Mode Row */}
-
- {/* Search Input */}
-
-
-
-
-
- {/* View Mode Toggle */}
-
-
-
-
-
-
- {/* Category Tabs (Desktop) */}
-
- {categories.map(({ id, label, count }) => {
- const Icon = CATEGORY_ICONS[id];
- const isSelected = selectedCategory === id;
-
- return (
-
- );
- })}
-
-
- {/* Category Dropdown (Mobile) */}
-
-
-
- {showCategoryDropdown && (
-
- {categories.map(({ id, label, count }) => {
- const Icon = CATEGORY_ICONS[id];
- const isSelected = selectedCategory === id;
-
- return (
-
- );
- })}
-
- )}
-
-
- );
-}
-
-export default AutomationFilters;
diff --git a/desktop/src/components/Automation/AutomationPanel.tsx b/desktop/src/components/Automation/AutomationPanel.tsx
deleted file mode 100644
index 1993aa6..0000000
--- a/desktop/src/components/Automation/AutomationPanel.tsx
+++ /dev/null
@@ -1,653 +0,0 @@
-/**
- * AutomationPanel - Unified Automation Entry Point
- *
- * Combines Pipelines, Hands and Workflows into a single unified view,
- * with category filtering, batch operations, and scheduling.
- *
- * @module components/Automation/AutomationPanel
- */
-
-import { useState, useEffect, useCallback, useMemo } from 'react';
-import { useHandStore } from '../../store/handStore';
-import { useWorkflowStore } from '../../store/workflowStore';
-import {
- type AutomationItem,
- type CategoryType,
- type CategoryStats,
- adaptToAutomationItems,
- calculateCategoryStats,
- filterByCategory,
- searchAutomationItems,
-} from '../../types/automation';
-import { AutomationCard } from './AutomationCard';
-import { AutomationFilters } from './AutomationFilters';
-import { BatchActionBar } from './BatchActionBar';
-import { ScheduleEditor } from './ScheduleEditor';
-import { PipelinesPanel } from '../PipelinesPanel';
-import {
- Zap,
- RefreshCw,
- Plus,
- Calendar,
- Search,
- X,
- Package,
- Bot,
- Workflow,
- Trash2,
- Clock,
-} from 'lucide-react';
-import { useToast } from '../ui/Toast';
-import type { ScheduleInfo } from '../../types/automation';
-
-// === View Mode ===
-
-type ViewMode = 'grid' | 'list';
-
-// === Tab Type ===
-
-type AutomationTab = 'pipelines' | 'hands' | 'workflows';
-
-// === Component Props ===
-
-interface AutomationPanelProps {
- initialCategory?: CategoryType;
- initialTab?: AutomationTab;
- onSelect?: (item: AutomationItem) => void;
- showBatchActions?: boolean;
-}
-
-// === Tab Configuration ===
-
-const TAB_CONFIG: { key: AutomationTab; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
- { key: 'pipelines', label: 'Pipelines', icon: Package },
- { key: 'hands', label: 'Hands', icon: Bot },
- { key: 'workflows', label: 'Workflows', icon: Workflow },
-];
-
-// === Main Component ===
-
-export function AutomationPanel({
- initialCategory = 'all',
- initialTab = 'pipelines',
- onSelect,
- showBatchActions = true,
-}: AutomationPanelProps) {
- // Store state - use domain stores
- const hands = useHandStore((s) => s.hands);
- const workflows = useWorkflowStore((s) => s.workflows);
- const handLoading = useHandStore((s) => s.isLoading);
- const workflowLoading = useWorkflowStore((s) => s.isLoading);
- const isLoading = handLoading || workflowLoading;
- const loadHands = useHandStore((s) => s.loadHands);
- const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
- const triggerHand = useHandStore((s) => s.triggerHand);
- const triggerWorkflow = useWorkflowStore((s) => s.triggerWorkflow);
-
- // UI state
- const [activeTab, setActiveTab] = useState(initialTab);
- const [selectedCategory, setSelectedCategory] = useState(initialCategory);
- const [searchQuery, setSearchQuery] = useState('');
- const [viewMode, setViewMode] = useState('grid');
- const [selectedIds, setSelectedIds] = useState>(new Set());
- const [executingIds, setExecutingIds] = useState>(new Set());
- const [showWorkflowDialog, setShowWorkflowDialog] = useState(false);
- const [showSchedulerDialog, setShowSchedulerDialog] = useState(false);
- const [showBatchScheduleDialog, setShowBatchScheduleDialog] = useState(false);
- const [workflowName, setWorkflowName] = useState('');
- const [workflowDescription, setWorkflowDescription] = useState('');
- const [isCreating, setIsCreating] = useState(false);
- const [schedules, setSchedules] = useState>({});
-
- const { toast } = useToast();
-
- // Load data on mount
- useEffect(() => {
- loadHands();
- loadWorkflows();
- }, [loadHands, loadWorkflows]);
-
- // Adapt hands and workflows to automation items
- const automationItems = useMemo(() => {
- return adaptToAutomationItems(hands, workflows);
- }, [hands, workflows]);
-
- // Calculate category stats
- const categoryStats = useMemo(() => {
- return calculateCategoryStats(automationItems);
- }, [automationItems]);
-
- // Filter and search items
- const filteredItems = useMemo(() => {
- let items = filterByCategory(automationItems, selectedCategory);
- if (searchQuery.trim()) {
- items = searchAutomationItems(items, searchQuery);
- }
- // Filter by tab
- if (activeTab === 'hands') {
- items = items.filter(item => item.type === 'hand');
- } else if (activeTab === 'workflows') {
- items = items.filter(item => item.type === 'workflow');
- }
- return items;
- }, [automationItems, selectedCategory, searchQuery, activeTab]);
-
- // Selection handlers
- const handleSelect = useCallback((id: string, selected: boolean) => {
- setSelectedIds(prev => {
- const next = new Set(prev);
- if (selected) {
- next.add(id);
- } else {
- next.delete(id);
- }
- return next;
- });
- }, []);
-
- const handleSelectAll = useCallback(() => {
- setSelectedIds(new Set(filteredItems.map(item => item.id)));
- }, [filteredItems]);
-
- const handleDeselectAll = useCallback(() => {
- setSelectedIds(new Set());
- }, []);
-
- // Workflow dialog handlers
- const handleCreateWorkflow = useCallback(() => {
- setShowWorkflowDialog(true);
- setWorkflowName('');
- setWorkflowDescription('');
- }, []);
-
- const handleSchedulerManage = useCallback(() => {
- setShowSchedulerDialog(true);
- }, []);
-
- // Create workflow handler
- const handleWorkflowCreate = useCallback(async () => {
- if (!workflowName.trim()) {
- toast('请输入工作流名称', 'error');
- return;
- }
-
- setIsCreating(true);
- try {
- const createWorkflow = useWorkflowStore.getState().createWorkflow;
- const result = await createWorkflow({
- name: workflowName.trim(),
- description: workflowDescription.trim() || undefined,
- steps: [], // Empty workflow, user will add steps later
- });
-
- if (result) {
- toast(`工作流 "${result.name}" 创建成功`, 'success');
- setShowWorkflowDialog(false);
- setWorkflowName('');
- setWorkflowDescription('');
- // Reload workflows
- await loadWorkflows();
- }
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : '创建工作流失败';
- toast(errorMsg, 'error');
- } finally {
- setIsCreating(false);
- }
- }, [workflowName, workflowDescription, toast, loadWorkflows]);
-
- // Batch schedule handler
- const handleBatchSchedule = useCallback(() => {
- if (selectedIds.size === 0) {
- toast('请先选择要调度的项目', 'info');
- return;
- }
- setShowBatchScheduleDialog(true);
- }, [selectedIds.size, toast]);
-
- // Save batch schedule
- const handleSaveBatchSchedule = useCallback((schedule: ScheduleInfo) => {
- // Save schedule for all selected items
- const newSchedules: Record = {};
- selectedIds.forEach(id => {
- newSchedules[id] = schedule;
- });
-
- setSchedules(prev => ({ ...prev, ...newSchedules }));
- setShowBatchScheduleDialog(false);
- setSelectedIds(new Set());
-
- const frequencyLabels = {
- once: '一次性',
- daily: '每天',
- weekly: '每周',
- monthly: '每月',
- custom: '自定义',
- };
-
- toast(`已为 ${Object.keys(newSchedules).length} 个项目设置${frequencyLabels[schedule.frequency]}调度`, 'success');
- }, [selectedIds, toast]);
-
- // Delete schedule
- const handleDeleteSchedule = useCallback((itemId: string) => {
- setSchedules(prev => {
- const { [itemId]: _, ...rest } = prev;
- return rest;
- });
- toast('调度已删除', 'success');
- }, [toast]);
-
- // Toggle schedule enabled
- const handleToggleScheduleEnabled = useCallback((itemId: string, enabled: boolean) => {
- setSchedules(prev => ({
- ...prev,
- [itemId]: { ...prev[itemId], enabled },
- }));
- }, []);
-
- // Execute handler
- const handleExecute = useCallback(async (item: AutomationItem, params?: Record) => {
- setExecutingIds(prev => new Set(prev).add(item.id));
-
- try {
- if (item.type === 'hand') {
- await triggerHand(item.id, params);
- } else {
- await triggerWorkflow(item.id, params);
- }
- toast(`${item.name} 执行成功`, 'success');
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err);
- toast(`${item.name} 执行失败: ${errorMsg}`, 'error');
- } finally {
- setExecutingIds(prev => {
- const next = new Set(prev);
- next.delete(item.id);
- return next;
- });
- }
- }, [triggerHand, triggerWorkflow, toast]);
-
- // Batch execute
- const handleBatchExecute = useCallback(async () => {
- const itemsToExecute = filteredItems.filter(item => selectedIds.has(item.id));
- let successCount = 0;
- let failCount = 0;
-
- for (const item of itemsToExecute) {
- try {
- if (item.type === 'hand') {
- await triggerHand(item.id);
- } else {
- await triggerWorkflow(item.id);
- }
- successCount++;
- } catch {
- failCount++;
- }
- }
-
- if (successCount > 0) {
- toast(`成功执行 ${successCount} 个项目`, 'success');
- }
- if (failCount > 0) {
- toast(`${failCount} 个项目执行失败`, 'error');
- }
-
- setSelectedIds(new Set());
- }, [filteredItems, selectedIds, triggerHand, triggerWorkflow, toast]);
-
- // Refresh handler
- const handleRefresh = useCallback(async () => {
- await Promise.all([loadHands(), loadWorkflows()]);
- toast('数据已刷新', 'success');
- }, [loadHands, loadWorkflows, toast]);
-
- // If Pipelines tab is active, show PipelinesPanel directly
- if (activeTab === 'pipelines') {
- return (
-
- {/* Header with Tabs */}
-
-
- {/* Tab Switcher */}
-
- {TAB_CONFIG.map(({ key, label, icon: Icon }) => (
-
- ))}
-
-
-
- {/* Pipelines Panel */}
-
-
- );
- }
-
- // Hands and Workflows tabs
- return (
-
- {/* Header */}
-
-
-
-
- 自动化
-
-
- ({automationItems.length})
-
-
-
- {/* Tab Switcher */}
-
- {TAB_CONFIG.map(({ key, label, icon: Icon }) => (
-
- ))}
-
-
-
-
-
-
-
- {/* Filters */}
-
-
- {/* Content */}
-
- {isLoading && automationItems.length === 0 ? (
-
-
-
- ) : filteredItems.length === 0 ? (
-
-
-
- {searchQuery ? '没有找到匹配的项目' : '暂无自动化项目'}
-
-
- ) : (
-
- {filteredItems.map(item => (
-
handleSelect(item.id, selected)}
- onExecute={(params) => handleExecute(item, params)}
- onClick={() => onSelect?.(item)}
- />
- ))}
-
- )}
-
-
- {/* Batch Actions */}
- {showBatchActions && selectedIds.size > 0 && (
-
- )}
-
- {/* Create Workflow Dialog */}
- {showWorkflowDialog && (
-
-
-
-
新建工作流
-
-
-
-
-
-
- setWorkflowName(e.target.value)}
- placeholder="输入工作流名称..."
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
- disabled={isCreating}
- />
-
-
-
-
-
- 创建后可在工作流编辑器中添加步骤
-
-
-
-
-
-
-
-
-
- )}
-
- {/* Scheduler Dialog */}
- {showSchedulerDialog && (
-
-
-
-
-
-
调度管理
-
-
-
-
- {Object.keys(schedules).length === 0 ? (
-
-
-
暂无调度任务
-
选择自动化项目后点击"批量调度"来创建定时任务
-
- ) : (
-
- {Object.entries(schedules).map(([itemId, schedule]) => {
- const item = automationItems.find(i => i.id === itemId);
- if (!item) return null;
-
- const frequencyLabels = {
- once: '一次性',
- daily: '每天',
- weekly: '每周',
- monthly: '每月',
- custom: '自定义',
- };
-
- const timeStr = `${schedule.time.hour.toString().padStart(2, '0')}:${schedule.time.minute.toString().padStart(2, '0')}`;
-
- return (
-
-
-
-
-
-
-
- {item.name}
-
-
- {frequencyLabels[schedule.frequency]} · {timeStr}
- {schedule.nextRun && ` · 下次: ${new Date(schedule.nextRun).toLocaleString()}`}
-
-
-
-
- {/* Toggle Enabled */}
-
- {/* Delete */}
-
-
-
- );
- })}
-
- )}
-
-
-
- 共 {Object.keys(schedules).length} 个调度任务
-
-
-
-
-
- )}
-
- {/* Batch Schedule Dialog */}
- {showBatchScheduleDialog && (
-
setShowBatchScheduleDialog(false)}
- />
- )}
-
- );
-}
-
-export default AutomationPanel;
diff --git a/desktop/src/components/Automation/BatchActionBar.tsx b/desktop/src/components/Automation/BatchActionBar.tsx
deleted file mode 100644
index 69b9b47..0000000
--- a/desktop/src/components/Automation/BatchActionBar.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-/**
- * BatchActionBar - Batch Operations Action Bar
- *
- * Provides batch action buttons for selected automation items.
- * Supports batch execute, approve, reject, and schedule.
- *
- * @module components/Automation/BatchActionBar
- */
-
-import { useState, useCallback } from 'react';
-import {
- Play,
- Check,
- X,
- Clock,
- XCircle,
- MoreHorizontal,
- Trash2,
- Copy,
-} from 'lucide-react';
-
-// === Component Props ===
-
-interface BatchActionBarProps {
- selectedCount: number;
- totalCount?: number; // Optional - for "select all X items" display
- onSelectAll: () => void;
- onDeselectAll: () => void;
- onBatchExecute: () => Promise;
- onBatchApprove?: () => Promise;
- onBatchReject?: () => Promise;
- onBatchSchedule?: () => void;
- onBatchDelete?: () => Promise;
- onBatchDuplicate?: () => Promise;
-}
-
-// === Main Component ===
-
-export function BatchActionBar({
- selectedCount,
- totalCount: _totalCount, // Used for future "select all X items" display
- onSelectAll,
- onDeselectAll,
- onBatchExecute,
- onBatchApprove,
- onBatchReject,
- onBatchSchedule,
- onBatchDelete,
- onBatchDuplicate,
-}: BatchActionBarProps) {
- const [isExecuting, setIsExecuting] = useState(false);
- const [showMoreMenu, setShowMoreMenu] = useState(false);
-
- // Handle batch execute
- const handleExecute = useCallback(async () => {
- setIsExecuting(true);
- try {
- await onBatchExecute();
- } finally {
- setIsExecuting(false);
- }
- }, [onBatchExecute]);
-
- // Handle batch approve
- const handleApprove = useCallback(async () => {
- if (onBatchApprove) {
- setIsExecuting(true);
- try {
- await onBatchApprove();
- } finally {
- setIsExecuting(false);
- }
- }
- }, [onBatchApprove]);
-
- // Handle batch reject
- const handleReject = useCallback(async () => {
- if (onBatchReject) {
- setIsExecuting(true);
- try {
- await onBatchReject();
- } finally {
- setIsExecuting(false);
- }
- }
- }, [onBatchReject]);
-
- return (
-
-
- {/* Selection Info */}
-
-
- 已选择 {selectedCount} 项
-
-
-
- |
-
-
-
-
- {/* Action Buttons */}
-
- {/* Execute */}
-
-
- {/* Approve (if handler provided) */}
- {onBatchApprove && (
-
- )}
-
- {/* Reject (if handler provided) */}
- {onBatchReject && (
-
- )}
-
- {/* Schedule */}
- {onBatchSchedule && (
-
- )}
-
- {/* More Actions */}
- {(onBatchDelete || onBatchDuplicate) && (
-
-
-
- {showMoreMenu && (
-
- {onBatchDuplicate && (
-
- )}
- {onBatchDelete && (
-
- )}
-
- )}
-
- )}
-
- {/* Close */}
-
-
-
-
- );
-}
-
-export default BatchActionBar;
diff --git a/desktop/src/components/Automation/ExecutionResult.tsx b/desktop/src/components/Automation/ExecutionResult.tsx
deleted file mode 100644
index 7051e28..0000000
--- a/desktop/src/components/Automation/ExecutionResult.tsx
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * ExecutionResult - Execution Result Display Component
- *
- * Displays the result of hand or workflow executions with
- * status, output, and error information.
- *
- * @module components/Automation/ExecutionResult
- */
-
-import { useState, useCallback, useMemo } from 'react';
-import type { RunInfo } from '../../types/automation';
-import {
- CheckCircle,
- XCircle,
- Clock,
- AlertTriangle,
- ChevronDown,
- ChevronUp,
- Copy,
- Download,
- RefreshCw,
- ExternalLink,
- FileText,
- Code,
- Image,
- FileSpreadsheet,
-} from 'lucide-react';
-import { useToast } from '../ui/Toast';
-
-// === Status Config ===
-
-const STATUS_CONFIG = {
- completed: {
- label: '完成',
- icon: CheckCircle,
- className: 'text-green-500',
- bgClass: 'bg-green-50 dark:bg-green-900/20',
- },
- failed: {
- label: '失败',
- icon: XCircle,
- className: 'text-red-500',
- bgClass: 'bg-red-50 dark:bg-red-900/20',
- },
- running: {
- label: '运行中',
- icon: RefreshCw,
- className: 'text-blue-500 animate-spin',
- bgClass: 'bg-blue-50 dark:bg-blue-900/20',
- },
- needs_approval: {
- label: '待审批',
- icon: AlertTriangle,
- className: 'text-yellow-500',
- bgClass: 'bg-yellow-50 dark:bg-yellow-900/20',
- },
- cancelled: {
- label: '已取消',
- icon: XCircle,
- className: 'text-gray-500',
- bgClass: 'bg-gray-50 dark:bg-gray-900/20',
- },
-};
-
-// === Component Props ===
-
-interface ExecutionResultProps {
- run: RunInfo;
- itemType: 'hand' | 'workflow';
- itemName: string;
- onRerun?: () => void;
- onViewDetails?: () => void;
- compact?: boolean;
-}
-
-// === Helper Functions ===
-
-function formatDuration(startedAt: string, completedAt?: string): string {
- const start = new Date(startedAt).getTime();
- const end = completedAt ? new Date(completedAt).getTime() : Date.now();
- const diffMs = end - start;
-
- const seconds = Math.floor(diffMs / 1000);
- const minutes = Math.floor(seconds / 60);
- const hours = Math.floor(minutes / 60);
-
- if (hours > 0) {
- return `${hours}h ${minutes % 60}m`;
- }
- if (minutes > 0) {
- return `${minutes}m ${seconds % 60}s`;
- }
- return `${seconds}s`;
-}
-
-function detectOutputType(output: unknown): 'text' | 'json' | 'markdown' | 'code' | 'image' | 'data' {
- if (!output) return 'text';
-
- if (typeof output === 'string') {
- // Check for image URL
- if (output.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i)) {
- return 'image';
- }
- // Check for markdown
- if (output.includes('#') || output.includes('**') || output.includes('```')) {
- return 'markdown';
- }
- // Check for code
- if (output.includes('function ') || output.includes('import ') || output.includes('class ')) {
- return 'code';
- }
- // Try to parse as JSON
- try {
- JSON.parse(output);
- return 'json';
- } catch {
- return 'text';
- }
- }
-
- // Object/array types
- if (typeof output === 'object') {
- return 'json';
- }
-
- return 'text';
-}
-
-function formatOutput(output: unknown, type: string): string {
- if (!output) return '无输出';
-
- if (type === 'json') {
- try {
- return JSON.stringify(output, null, 2);
- } catch {
- return String(output);
- }
- }
-
- return String(output);
-}
-
-// === Output Viewer Component ===
-
-interface OutputViewerProps {
- output: unknown;
- type: string;
-}
-
-function OutputViewer({ output, type }: OutputViewerProps) {
- const [copied, setCopied] = useState(false);
- const { toast } = useToast();
-
- const handleCopy = useCallback(async () => {
- const text = formatOutput(output, type);
- await navigator.clipboard.writeText(text);
- setCopied(true);
- toast('已复制到剪贴板', 'success');
- setTimeout(() => setCopied(false), 2000);
- }, [output, type, toast]);
-
- const handleDownload = useCallback(() => {
- const text = formatOutput(output, type);
- const blob = new Blob([text], { type: 'text/plain' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `output-${Date.now()}.${type === 'json' ? 'json' : 'txt'}`;
- a.click();
- URL.revokeObjectURL(url);
- }, [output, type]);
-
- // Image preview
- if (type === 'image' && typeof output === 'string') {
- return (
-
-

-
-
-
-
- );
- }
-
- // Text/JSON/Code output
- const content = formatOutput(output, type);
-
- return (
-
-
- {content}
-
-
-
-
-
-
- );
-}
-
-// === Main Component ===
-
-export function ExecutionResult({
- run,
- itemType,
- itemName,
- onRerun,
- onViewDetails,
- compact = false,
-}: ExecutionResultProps) {
- const [expanded, setExpanded] = useState(!compact);
-
- const statusConfig = STATUS_CONFIG[run.status as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.completed;
- const StatusIcon = statusConfig.icon;
-
- // Safely extract error message as string
- const getErrorMessage = (): string | null => {
- if (typeof run.error === 'string' && run.error.length > 0) {
- return run.error;
- }
- return null;
- };
- const errorMessage = getErrorMessage();
-
- const outputType = useMemo(() => detectOutputType(run.output), [run.output]);
- const duration = useMemo(() => {
- if (run.duration) return `${run.duration}s`;
- if (run.completedAt && run.startedAt) {
- return formatDuration(run.startedAt, run.completedAt);
- }
- return null;
- }, [run.duration, run.startedAt, run.completedAt]);
-
- // Compact mode
- if (compact && !expanded) {
- return (
- setExpanded(true)}
- >
-
-
-
-
- {itemName}
-
-
- {statusConfig.label}
-
-
- {duration && (
-
- 耗时: {duration}
-
- )}
-
-
-
- );
- }
-
- return (
-
- {/* Header */}
-
setExpanded(false) : undefined}
- >
-
-
-
-
-
- {itemName}
-
-
- {statusConfig.label}
-
-
- {itemType === 'hand' ? '自主能力' : '工作流'}
-
-
- {run.runId && (
-
- 执行ID: {run.runId}
-
- )}
-
-
-
-
- {duration && (
-
- 耗时: {duration}
-
- )}
- {compact && (
-
- )}
-
-
-
- {/* Body */}
- {expanded && (
-
- {/* Error */}
- {(() => {
- if (!errorMessage) return null;
- return (
-
-
错误信息
-
{errorMessage}
-
- );
- })()}
-
- {/* Output */}
- {run.output !== undefined && run.output !== null && (
-
-
-
输出结果
-
- {outputType === 'json' && }
- {outputType === 'markdown' && }
- {outputType === 'image' && }
- {outputType === 'data' && }
- {outputType.toUpperCase()}
-
-
-
-
- )}
-
- {/* Timestamps */}
-
-
-
- 开始: {new Date(run.startedAt).toLocaleString('zh-CN')}
-
- {run.completedAt && (
-
- 完成: {new Date(run.completedAt).toLocaleString('zh-CN')}
-
- )}
-
-
- {/* Actions */}
-
- {onRerun && (
-
- )}
- {onViewDetails && (
-
- )}
-
-
- )}
-
- );
-}
-
-export default ExecutionResult;
diff --git a/desktop/src/components/Automation/ScheduleEditor.tsx b/desktop/src/components/Automation/ScheduleEditor.tsx
deleted file mode 100644
index 694d6b3..0000000
--- a/desktop/src/components/Automation/ScheduleEditor.tsx
+++ /dev/null
@@ -1,378 +0,0 @@
-/**
- * ScheduleEditor - Visual Schedule Configuration
- *
- * Provides a visual interface for configuring schedules
- * without requiring knowledge of cron syntax.
- *
- * @module components/Automation/ScheduleEditor
- */
-
-import { useState, useCallback, useMemo } from 'react';
-import type { ScheduleInfo } from '../../types/automation';
-import {
- Calendar,
- Info,
-} from 'lucide-react';
-import { useToast } from '../ui/Toast';
-
-// === Frequency Types ===
-
-type Frequency = 'once' | 'daily' | 'weekly' | 'monthly' | 'custom';
-
-// === Timezones ===
-
-const COMMON_TIMEZONES = [
- { value: 'Asia/Shanghai', label: '北京时间 (UTC+8)' },
- { value: 'Asia/Tokyo', label: '东京时间 (UTC+9)' },
- { value: 'Asia/Singapore', label: '新加坡时间 (UTC+8)' },
- { value: 'America/New_York', label: '纽约时间 (UTC-5)' },
- { value: 'America/Los_Angeles', label: '洛杉矶时间 (UTC-8)' },
- { value: 'Europe/London', label: '伦敦时间 (UTC+0)' },
- { value: 'UTC', label: '协调世界时 (UTC)' },
-];
-
-// === Day Names ===
-
-const DAY_NAMES = ['日', '一', '二', '三', '四', '五', '六'];
-const DAY_NAMES_FULL = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
-
-// === Component Props ===
-
-interface ScheduleEditorProps {
- schedule?: ScheduleInfo;
- onSave: (schedule: ScheduleInfo) => void;
- onCancel: () => void;
- itemName?: string;
-}
-
-// === Helper Functions ===
-
-function formatSchedulePreview(schedule: ScheduleInfo): string {
- const { frequency, time, daysOfWeek, dayOfMonth, timezone } = schedule;
- const timeStr = `${time.hour.toString().padStart(2, '0')}:${time.minute.toString().padStart(2, '0')}`;
- const tzLabel = COMMON_TIMEZONES.find(tz => tz.value === timezone)?.label || timezone;
-
- switch (frequency) {
- case 'once':
- return `一次性执行于 ${timeStr} (${tzLabel})`;
- case 'daily':
- return `每天 ${timeStr} (${tzLabel})`;
- case 'weekly':
- const days = (daysOfWeek || []).map(d => DAY_NAMES_FULL[d]).join('、');
- return `每${days} ${timeStr} (${tzLabel})`;
- case 'monthly':
- return `每月${dayOfMonth || 1}日 ${timeStr} (${tzLabel})`;
- case 'custom':
- return schedule.customCron || '自定义调度';
- default:
- return '未设置';
- }
-}
-
-// === Main Component ===
-
-export function ScheduleEditor({
- schedule,
- onSave,
- onCancel,
- itemName = '自动化项目',
-}: ScheduleEditorProps) {
- const { toast } = useToast();
-
- // Initialize state from existing schedule
- const [frequency, setFrequency] = useState(schedule?.frequency || 'daily');
- const [time, setTime] = useState(schedule?.time || { hour: 9, minute: 0 });
- const [daysOfWeek, setDaysOfWeek] = useState(schedule?.daysOfWeek || [1, 2, 3, 4, 5]);
- const [dayOfMonth, setDayOfMonth] = useState(schedule?.dayOfMonth || 1);
- const [timezone, setTimezone] = useState(schedule?.timezone || 'Asia/Shanghai');
- const [endDate, setEndDate] = useState(schedule?.endDate || '');
- const [customCron, setCustomCron] = useState(schedule?.customCron || '');
- const [enabled, setEnabled] = useState(schedule?.enabled ?? true);
-
- // Toggle day of week
- const toggleDayOfWeek = useCallback((day: number) => {
- setDaysOfWeek(prev =>
- prev.includes(day)
- ? prev.filter(d => d !== day)
- : [...prev, day].sort()
- );
- }, []);
-
- // Handle save
- const handleSave = useCallback(() => {
- // Validate
- if (frequency === 'weekly' && daysOfWeek.length === 0) {
- toast('请选择至少一个重复日期', 'error');
- return;
- }
-
- if (frequency === 'custom' && !customCron) {
- toast('请输入自定义 cron 表达式', 'error');
- return;
- }
-
- const newSchedule: ScheduleInfo = {
- enabled,
- frequency,
- time,
- daysOfWeek: frequency === 'weekly' ? daysOfWeek : undefined,
- dayOfMonth: frequency === 'monthly' ? dayOfMonth : undefined,
- customCron: frequency === 'custom' ? customCron : undefined,
- timezone,
- endDate: endDate || undefined,
- };
-
- onSave(newSchedule);
- toast('调度设置已保存', 'success');
- }, [frequency, daysOfWeek, customCron, enabled, time, dayOfMonth, timezone, endDate, onSave, toast]);
-
- // Generate preview
- const preview = useMemo(() => {
- return formatSchedulePreview({
- enabled,
- frequency,
- time,
- daysOfWeek,
- dayOfMonth,
- customCron,
- timezone,
- });
- }, [enabled, frequency, time, daysOfWeek, dayOfMonth, customCron, timezone]);
-
- return (
-
- {/* Backdrop */}
-
-
- {/* Modal */}
-
- {/* Header */}
-
-
-
-
-
-
-
- 调度设置
-
- {itemName && (
-
{itemName}
- )}
-
-
-
-
-
- {/* Body */}
-
- {/* Enable Toggle */}
-
-
-
- 启用调度
-
-
- 开启后,此项目将按照设定的时间自动执行
-
-
-
-
-
- {/* Frequency Selection */}
-
-
-
- {[
- { value: 'once', label: '一次' },
- { value: 'daily', label: '每天' },
- { value: 'weekly', label: '每周' },
- { value: 'monthly', label: '每月' },
- { value: 'custom', label: '自定义' },
- ].map(option => (
-
- ))}
-
-
-
- {/* Time Selection */}
- {frequency !== 'custom' && (
-
-
-
-
-
-
-
-
- )}
-
- {/* Weekly Days Selection */}
- {frequency === 'weekly' && (
-
-
-
- {DAY_NAMES.map((day, index) => (
-
- ))}
-
-
- )}
-
- {/* Monthly Day Selection */}
- {frequency === 'monthly' && (
-
-
-
-
- )}
-
- {/* Custom Cron Input */}
- {frequency === 'custom' && (
-
-
-
setCustomCron(e.target.value)}
- placeholder="* * * * * (分 时 日 月 周)"
- className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white font-mono text-sm"
- />
-
-
- 示例: "0 9 * * *" 表示每天 9:00 执行
-
-
- )}
-
- {/* End Date */}
- {frequency !== 'once' && (
-
-
- setEndDate(e.target.value)}
- className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
- />
-
- )}
-
- {/* Preview */}
-
-
-
- {/* Footer */}
-
-
-
-
-
-
- );
-}
-
-export default ScheduleEditor;
diff --git a/desktop/src/components/Automation/index.ts b/desktop/src/components/Automation/index.ts
deleted file mode 100644
index 48808a4..0000000
--- a/desktop/src/components/Automation/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Automation Components
- *
- * Unified automation system components for Hands and Workflows.
- *
- * @module components/Automation
- */
-
-export { AutomationPanel, default as AutomationPanelDefault } from './AutomationPanel';
-export { AutomationCard } from './AutomationCard';
-export { AutomationFilters } from './AutomationFilters';
-export { BatchActionBar } from './BatchActionBar';
-export { ScheduleEditor } from './ScheduleEditor';
-export { ApprovalQueue } from './ApprovalQueue';
-export { ExecutionResult } from './ExecutionResult';
-
-// Re-export types
-export type {
- AutomationItem,
- AutomationStatus,
- AutomationType,
- CategoryType,
- CategoryStats,
- RunInfo,
- ScheduleInfo,
-} from '../../types/automation';
diff --git a/desktop/src/components/ChannelList.tsx b/desktop/src/components/ChannelList.tsx
deleted file mode 100644
index 7850619..0000000
--- a/desktop/src/components/ChannelList.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import { useEffect } from 'react';
-import { useConnectionStore } from '../store/connectionStore';
-import { useAgentStore } from '../store/agentStore';
-import { useConfigStore } from '../store/configStore';
-import { Radio, RefreshCw, MessageCircle, Settings } from 'lucide-react';
-
-const CHANNEL_ICONS: Record = {
- feishu: '飞',
- qqbot: 'QQ',
- wechat: '微',
-};
-
-// 可用频道类型(用于显示未配置的频道)
-const AVAILABLE_CHANNEL_TYPES = [
- { type: 'feishu', name: '飞书 (Feishu)' },
- { type: 'wechat', name: '微信' },
- { type: 'qqbot', name: 'QQ 机器人' },
-];
-
-interface ChannelListProps {
- onOpenSettings?: () => void;
-}
-
-export function ChannelList({ onOpenSettings }: ChannelListProps) {
- const connectionState = useConnectionStore((s) => s.connectionState);
- const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus);
- const channels = useConfigStore((s) => s.channels);
- const loadChannels = useConfigStore((s) => s.loadChannels);
-
- const connected = connectionState === 'connected';
-
- useEffect(() => {
- if (connected) {
- loadPluginStatus().then(() => loadChannels());
- }
- }, [connected]);
-
- const handleRefresh = () => {
- loadPluginStatus().then(() => loadChannels());
- };
-
- // 去重:基于 channel id
- const uniqueChannels = channels.filter((ch, index, self) =>
- index === self.findIndex(c => c.id === ch.id)
- );
-
- // 获取已配置的频道类型
- const configuredTypes = new Set(uniqueChannels.map(c => c.type));
-
- // 未配置的频道类型
- const unconfiguredTypes = AVAILABLE_CHANNEL_TYPES.filter(ct => !configuredTypes.has(ct.type));
-
- if (!connected) {
- return (
-
-
-
IM 频道
-
连接 Gateway 后可用
-
- );
- }
-
- return (
-
- {/* Header */}
-
- 频道列表
-
-
-
-
- {/* Configured channels */}
- {uniqueChannels.map((ch) => (
-
-
- {CHANNEL_ICONS[ch.type] || }
-
-
-
{ch.label}
-
- {ch.status === 'active' ? '已连接' : ch.status === 'error' ? ch.error || '错误' : '未配置'}
- {ch.accounts !== undefined && ch.accounts > 0 && ` · ${ch.accounts} 个账号`}
-
-
-
- ))}
-
- {/* Unconfigured channels - 只显示一次 */}
- {unconfiguredTypes.map((ct) => (
-
-
- {CHANNEL_ICONS[ct.type] || }
-
-
-
- ))}
-
- {/* Help text */}
-
-
在设置中配置 IM 频道
- {onOpenSettings && (
-
- )}
-
-
-
- );
-}
diff --git a/desktop/src/components/ConnectionStatus.tsx b/desktop/src/components/ConnectionStatus.tsx
deleted file mode 100644
index 5013ad8..0000000
--- a/desktop/src/components/ConnectionStatus.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-/**
- * ConnectionStatus Component
- *
- * Displays the current Gateway connection status with visual indicators.
- * Supports automatic reconnect and manual reconnect button.
- * Includes health status indicator for ZCLAW backend.
- */
-
-import { useState, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Wifi, WifiOff, Loader2, RefreshCw, Heart, HeartPulse } from 'lucide-react';
-import { useConnectionStore, getClient } from '../store/connectionStore';
-import {
- createHealthCheckScheduler,
- getHealthStatusLabel,
- formatHealthCheckTime,
- type HealthCheckResult,
- type HealthStatus,
-} from '../lib/health-check';
-
-interface ConnectionStatusProps {
- /** Show compact version (just icon and status text) */
- compact?: boolean;
- /** Show reconnect button when disconnected */
- showReconnectButton?: boolean;
- /** Additional CSS classes */
- className?: string;
-}
-
-interface ReconnectInfo {
- attempt: number;
- delay: number;
- maxAttempts: number;
-}
-
-type StatusType = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
-
-const statusConfig: Record = {
- disconnected: {
- color: 'text-red-500',
- bgColor: 'bg-red-50 dark:bg-red-900/20',
- label: '已断开',
- icon: WifiOff,
- },
- connecting: {
- color: 'text-yellow-500',
- bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
- label: '连接中...',
- icon: Loader2,
- animate: true,
- },
- handshaking: {
- color: 'text-yellow-500',
- bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
- label: '认证中...',
- icon: Loader2,
- animate: true,
- },
- connected: {
- color: 'text-green-500',
- bgColor: 'bg-green-50 dark:bg-green-900/20',
- label: '已连接',
- icon: Wifi,
- },
- reconnecting: {
- color: 'text-orange-500',
- bgColor: 'bg-orange-50 dark:bg-orange-900/20',
- label: '重连中...',
- icon: RefreshCw,
- animate: true,
- },
-};
-
-export function ConnectionStatus({
- compact = false,
- showReconnectButton = true,
- className = '',
-}: ConnectionStatusProps) {
- const connectionState = useConnectionStore((s) => s.connectionState);
- const connect = useConnectionStore((s) => s.connect);
- const [showPrompt, setShowPrompt] = useState(false);
- const [reconnectInfo, setReconnectInfo] = useState(null);
-
- // Listen for reconnect events
- useEffect(() => {
- const client = getClient();
-
- const unsubReconnecting = client.on('reconnecting', (info) => {
- setReconnectInfo(info as ReconnectInfo);
- });
-
- const unsubFailed = client.on('reconnect_failed', () => {
- setShowPrompt(true);
- setReconnectInfo(null);
- });
-
- const unsubConnected = client.on('connected', () => {
- setShowPrompt(false);
- setReconnectInfo(null);
- });
-
- return () => {
- unsubReconnecting();
- unsubFailed();
- unsubConnected();
- };
- }, []);
-
- const config = statusConfig[connectionState];
- const Icon = config.icon;
- const isDisconnected = connectionState === 'disconnected';
- const isReconnecting = connectionState === 'reconnecting';
-
- const handleReconnect = async () => {
- setShowPrompt(false);
- try {
- await connect();
- } catch (error) {
- console.error('Manual reconnect failed:', error);
- }
- };
-
- // Compact version
- if (compact) {
- return (
-
-
-
- {isReconnecting && reconnectInfo
- ? `${config.label} (${reconnectInfo.attempt}/${reconnectInfo.maxAttempts})`
- : config.label}
-
- {showPrompt && showReconnectButton && (
-
- )}
-
- );
- }
-
- // Full version
- return (
-
-
-
-
-
-
-
- {isReconnecting && reconnectInfo
- ? `${config.label} (${reconnectInfo.attempt}/${reconnectInfo.maxAttempts})`
- : config.label}
-
- {reconnectInfo && (
-
- {Math.round(reconnectInfo.delay / 1000)}秒后重试
-
- )}
-
-
-
- {showPrompt && isDisconnected && showReconnectButton && (
-
-
- 重新连接
-
- )}
-
-
- );
-}
-
-/**
- * ConnectionIndicator - Minimal connection indicator for headers
- */
-export function ConnectionIndicator({ className = '' }: { className?: string }) {
- const connectionState = useConnectionStore((s) => s.connectionState);
-
- const isConnected = connectionState === 'connected';
- const isReconnecting = connectionState === 'reconnecting';
-
- return (
-
-
-
- {isConnected
- ? 'Gateway 已连接'
- : isReconnecting
- ? '重连中...'
- : 'Gateway 未连接'}
-
-
- );
-}
-
-/**
- * HealthStatusIndicator - Displays ZCLAW backend health status
- */
-export function HealthStatusIndicator({
- className = '',
- showDetails = false,
-}: {
- className?: string;
- showDetails?: boolean;
-}) {
- const [healthResult, setHealthResult] = useState(null);
-
- useEffect(() => {
- // Start periodic health checks
- const cleanup = createHealthCheckScheduler((result) => {
- setHealthResult(result);
- }, 30000); // Check every 30 seconds
-
- return cleanup;
- }, []);
-
- if (!healthResult) {
- return (
-
-
- 检查中...
-
- );
- }
-
- const statusColors: Record = {
- healthy: { dot: 'bg-green-400', text: 'text-green-500', icon: Heart },
- unhealthy: { dot: 'bg-red-400', text: 'text-red-500', icon: HeartPulse },
- unknown: { dot: 'bg-gray-400', text: 'text-gray-500', icon: Heart },
- };
-
- const config = statusColors[healthResult.status];
- const Icon = config.icon;
-
- return (
-
-
-
- {getHealthStatusLabel(healthResult.status)}
-
- {showDetails && healthResult.message && (
-
- ({formatHealthCheckTime(healthResult.timestamp)})
-
- )}
-
- );
-}
-
-export default ConnectionStatus;
diff --git a/desktop/src/components/ErrorNotification.tsx b/desktop/src/components/ErrorNotification.tsx
deleted file mode 100644
index d06a197..0000000
--- a/desktop/src/components/ErrorNotification.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- * ErrorNotification Component
- *
- * Displays error notifications as toast-style messages.
- * Integrates with the centralized error handling system.
- */
-
-import { useState, useEffect, useCallback } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import {
- X,
- AlertCircle,
- AlertTriangle,
- Bug,
- WifiOff,
- ShieldAlert,
- Clock,
- ChevronDown,
- ChevronUp,
-} from 'lucide-react';
-import {
- getUndismissedErrors,
- dismissError,
- dismissAll,
- type StoredError,
-} from '../lib/error-handling';
-import {
- ErrorCategory,
- ErrorSeverity,
- formatErrorForToast,
-} from '../lib/error-types';
-
-interface ErrorNotificationProps {
- /** Maximum number of visible notifications */
- maxVisible?: number;
- /** Position on screen */
- position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
- /** Auto dismiss timeout in ms (0 = no auto dismiss) */
- autoDismissMs?: number;
- /** Additional CSS classes */
- className?: string;
-}
-
-const categoryIcons: Record = {
- network: WifiOff,
- auth: ShieldAlert,
- permission: ShieldAlert,
- validation: AlertTriangle,
- config: AlertTriangle,
- server: Bug,
- client: AlertCircle,
- timeout: Clock,
- system: Bug,
-};
-
-const severityColors: Record = {
- critical: {
- bg: 'bg-red-50 dark:bg-red-900/20',
- border: 'border-red-200 dark:border-red-800',
- text: 'text-red-800 dark:text-red-200',
- icon: 'text-red-500',
- },
- high: {
- bg: 'bg-orange-50 dark:bg-orange-900/20',
- border: 'border-orange-200 dark:border-orange-800',
- text: 'text-orange-800 dark:text-orange-200',
- icon: 'text-orange-500',
- },
- medium: {
- bg: 'bg-yellow-50 dark:bg-yellow-900/20',
- border: 'border-yellow-200 dark:border-yellow-800',
- text: 'text-yellow-800 dark:text-yellow-200',
- icon: 'text-yellow-500',
- },
- low: {
- bg: 'bg-blue-50 dark:bg-blue-900/20',
- border: 'border-blue-200 dark:border-blue-800',
- text: 'text-blue-800 dark:text-blue-200',
- icon: 'text-blue-500',
- },
-};
-
-function ErrorItem({
- error,
- onDismiss,
- autoDismissMs,
-}: {
- error: StoredError;
- onDismiss: (id: string) => void;
- autoDismissMs: number;
-}) {
- const [expanded, setExpanded] = useState(false);
- const Icon = categoryIcons[error.category] || AlertCircle;
- const colors = severityColors[error.severity] || severityColors.medium;
- const { title, message } = formatErrorForToast(error);
-
- // Auto dismiss
- useEffect(() => {
- if (autoDismissMs > 0 && error.severity !== 'critical') {
- const timer = setTimeout(() => {
- onDismiss(error.id);
- }, autoDismissMs);
- return () => clearTimeout(timer);
- }
- }, [autoDismissMs, error.id, error.severity, onDismiss]);
-
- const hasDetails = error.stack || error.context;
-
- return (
-
-
-
-
-
-
{title}
-
-
-
{message}
-
- {hasDetails && (
-
- )}
-
- {expanded && hasDetails && (
-
- {error.context && (
-
- Context:
- {JSON.stringify(error.context, null, 2)}
-
- )}
- {error.stack && (
- {error.stack}
- )}
-
- )}
-
-
- {error.category}
- •
- {new Date(error.timestamp).toLocaleTimeString()}
-
-
-
-
- );
-}
-
-export function ErrorNotification({
- maxVisible = 3,
- position = 'top-right',
- autoDismissMs = 10000,
- className = '',
-}: ErrorNotificationProps) {
- const [errors, setErrors] = useState([]);
-
- // Poll for new errors
- useEffect(() => {
- const updateErrors = () => {
- setErrors(getUndismissedErrors().slice(0, maxVisible));
- };
-
- updateErrors();
- const interval = setInterval(updateErrors, 1000);
- return () => clearInterval(interval);
- }, [maxVisible]);
-
- const handleDismiss = useCallback((id: string) => {
- dismissError(id);
- setErrors(prev => prev.filter(e => e.id !== id));
- }, []);
-
- const handleDismissAll = useCallback(() => {
- dismissAll();
- setErrors([]);
- }, []);
-
- const positionClasses: Record = {
- 'top-right': 'top-4 right-4',
- 'top-left': 'top-4 left-4',
- 'bottom-right': 'bottom-4 right-4',
- 'bottom-left': 'bottom-4 left-4',
- };
-
- if (errors.length === 0) return null;
-
- return (
-
-
- {errors.map(error => (
-
- ))}
-
-
- {errors.length > 1 && (
-
- 清除全部 ({errors.length})
-
- )}
-
- );
-}
-
-/**
- * ErrorNotificationProvider - Include at app root
- */
-export function ErrorNotificationProvider({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
- <>
- {children}
-
- >
- );
-}
-
-export default ErrorNotification;
diff --git a/desktop/src/components/HandList.tsx b/desktop/src/components/HandList.tsx
deleted file mode 100644
index d3e16d7..0000000
--- a/desktop/src/components/HandList.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * HandList - 左侧导航的 Hands 列表
- *
- * 显示所有可用的 Hands(自主能力包),
- * 允许用户选择一个 Hand 来查看其任务和结果。
- */
-
-import { useEffect } from 'react';
-import { useHandStore, type Hand } from '../store/handStore';
-import { Zap, Loader2, RefreshCw, CheckCircle, XCircle, AlertTriangle } from 'lucide-react';
-
-interface HandListProps {
- selectedHandId?: string;
- onSelectHand?: (handId: string) => void;
-}
-
-// 状态图标
-function HandStatusIcon({ status }: { status: Hand['status'] }) {
- switch (status) {
- case 'running':
- return ;
- case 'needs_approval':
- return ;
- case 'error':
- return ;
- case 'setup_needed':
- case 'unavailable':
- return ;
- default:
- return ;
- }
-}
-
-// 状态标签
-const STATUS_LABELS: Record = {
- idle: '就绪',
- running: '运行中',
- needs_approval: '待审批',
- error: '错误',
- unavailable: '不可用',
- setup_needed: '需配置',
-};
-
-export function HandList({ selectedHandId, onSelectHand }: HandListProps) {
- const hands = useHandStore((s) => s.hands);
- const loadHands = useHandStore((s) => s.loadHands);
- const isLoading = useHandStore((s) => s.isLoading);
-
- useEffect(() => {
- loadHands();
- }, [loadHands]);
-
- if (isLoading && hands.length === 0) {
- return (
-
- );
- }
-
- if (hands.length === 0) {
- return (
-
-
-
暂无可用 Hands
-
连接 ZCLAW 后显示
-
- );
- }
-
- return (
-
- {/* 头部 */}
-
-
-
自主能力包
-
{hands.length} 个可用
-
-
-
-
- {/* Hands 列表 */}
-
- {hands.map((hand) => (
-
- ))}
-
-
- );
-}
-
-export default HandList;
diff --git a/desktop/src/components/HandTaskPanel.tsx b/desktop/src/components/HandTaskPanel.tsx
deleted file mode 100644
index 4d4235c..0000000
--- a/desktop/src/components/HandTaskPanel.tsx
+++ /dev/null
@@ -1,326 +0,0 @@
-/**
- * HandTaskPanel - Hand 任务和结果面板
- *
- * 显示选中 Hand 的任务清单、执行历史和结果。
- * 使用真实 API 数据,移除了 Mock 数据。
- */
-
-import { useState, useEffect, useCallback } from 'react';
-import { useHandStore, type Hand, type HandRun } from '../store/handStore';
-import {
- Zap,
- Loader2,
- Clock,
- CheckCircle,
- XCircle,
- AlertCircle,
- ChevronRight,
- Play,
- ArrowLeft,
- RefreshCw,
-} from 'lucide-react';
-import { useToast } from './ui/Toast';
-
-interface HandTaskPanelProps {
- handId: string;
- onBack?: () => void;
-}
-
-// 任务状态配置
-const RUN_STATUS_CONFIG: Record }> = {
- pending: { label: '等待中', className: 'text-gray-500 bg-gray-100', icon: Clock },
- running: { label: '运行中', className: 'text-blue-600 bg-blue-100', icon: Loader2 },
- completed: { label: '已完成', className: 'text-green-600 bg-green-100', icon: CheckCircle },
- failed: { label: '失败', className: 'text-red-600 bg-red-100', icon: XCircle },
- cancelled: { label: '已取消', className: 'text-gray-500 bg-gray-100', icon: XCircle },
- needs_approval: { label: '待审批', className: 'text-yellow-600 bg-yellow-100', icon: AlertCircle },
- success: { label: '成功', className: 'text-green-600 bg-green-100', icon: CheckCircle },
- error: { label: '错误', className: 'text-red-600 bg-red-100', icon: XCircle },
-};
-
-export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) {
- const hands = useHandStore((s) => s.hands);
- const handRuns = useHandStore((s) => s.handRuns);
- const loadHands = useHandStore((s) => s.loadHands);
- const loadHandRuns = useHandStore((s) => s.loadHandRuns);
- const triggerHand = useHandStore((s) => s.triggerHand);
- const isLoading = useHandStore((s) => s.isLoading);
- const { toast } = useToast();
- const [selectedHand, setSelectedHand] = useState(null);
- const [isActivating, setIsActivating] = useState(false);
- const [isRefreshing, setIsRefreshing] = useState(false);
-
- // Load hands on mount
- useEffect(() => {
- loadHands();
- }, [loadHands]);
-
- // Find selected hand
- useEffect(() => {
- const hand = hands.find(h => h.id === handId || h.name === handId);
- setSelectedHand(hand || null);
- }, [hands, handId]);
-
- // Load task history when hand is selected
- useEffect(() => {
- if (selectedHand) {
- loadHandRuns(selectedHand.id, { limit: 50 });
- }
- }, [selectedHand, loadHandRuns]);
-
- // Get runs for this hand from store
- const tasks: HandRun[] = selectedHand ? (handRuns[selectedHand.id] || []) : [];
-
- // Refresh task history
- const handleRefresh = useCallback(async () => {
- if (!selectedHand) return;
- setIsRefreshing(true);
- try {
- await loadHandRuns(selectedHand.id, { limit: 50 });
- } finally {
- setIsRefreshing(false);
- }
- }, [selectedHand, loadHandRuns]);
-
- // Trigger hand execution
- const handleActivate = useCallback(async () => {
- if (!selectedHand) return;
-
- // Check if hand is already running
- if (selectedHand.status === 'running') {
- toast(`Hand "${selectedHand.name}" 正在运行中,请等待完成`, 'warning');
- return;
- }
-
- setIsActivating(true);
-
- try {
- const result = await triggerHand(selectedHand.id);
-
- if (result) {
- toast(`Hand "${selectedHand.name}" 已成功启动`, 'success');
- // Refresh hands list and task history
- await Promise.all([
- loadHands(),
- loadHandRuns(selectedHand.id, { limit: 50 }),
- ]);
- } else {
- // Check for specific error in store
- const storeError = useHandStore.getState().error;
- if (storeError?.includes('already active')) {
- toast(`Hand "${selectedHand.name}" 已在运行中`, 'warning');
- } else {
- toast(`Hand "${selectedHand.name}" 启动失败: ${storeError || '未知错误'}`, 'error');
- }
- }
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err);
- console.error(`[HandTaskPanel] Activation error:`, errorMsg);
-
- if (errorMsg.includes('already active')) {
- toast(`Hand "${selectedHand.name}" 已在运行中`, 'warning');
- } else {
- toast(`Hand "${selectedHand.name}" 启动异常: ${errorMsg}`, 'error');
- }
- } finally {
- setIsActivating(false);
- }
- }, [selectedHand, triggerHand, loadHands, loadHandRuns, toast]);
-
- if (!selectedHand) {
- return (
-
- );
- }
-
- const runningTasks = tasks.filter(t => t.status === 'running');
- const completedTasks = tasks.filter(t => ['completed', 'success', 'failed', 'error', 'cancelled'].includes(t.status));
- const pendingTasks = tasks.filter(t => ['pending', 'needs_approval'].includes(t.status));
-
- return (
-
- {/* 头部 */}
-
-
- {onBack && (
-
- )}
-
{selectedHand.icon || '🤖'}
-
-
- {selectedHand.name}
-
-
{selectedHand.description}
-
-
-
-
-
-
- {/* 内容区域 */}
-
- {/* 加载状态 */}
- {isLoading && tasks.length === 0 && (
-
- )}
-
- {/* 运行中的任务 */}
- {runningTasks.length > 0 && (
-
-
-
- 运行中 ({runningTasks.length})
-
-
- {runningTasks.map(task => (
-
- ))}
-
-
- )}
-
- {/* 待处理任务 */}
- {pendingTasks.length > 0 && (
-
-
-
- 待处理 ({pendingTasks.length})
-
-
- {pendingTasks.map(task => (
-
- ))}
-
-
- )}
-
- {/* 已完成任务 */}
- {completedTasks.length > 0 && (
-
-
- 历史记录 ({completedTasks.length})
-
-
- {completedTasks.map(task => (
-
- ))}
-
-
- )}
-
- {/* 空状态 */}
- {!isLoading && tasks.length === 0 && (
-
-
-
-
-
暂无任务记录
-
- 点击"执行任务"按钮开始运行
-
-
- )}
-
-
- );
-}
-
-// 任务卡片组件
-function TaskCard({ task, expanded = false }: { task: HandRun; expanded?: boolean }) {
- const [isExpanded, setIsExpanded] = useState(expanded);
- const config = RUN_STATUS_CONFIG[task.status] || RUN_STATUS_CONFIG.pending;
- const StatusIcon = config.icon;
-
- // Format result for display
- const resultText = task.result
- ? (typeof task.result === 'string' ? task.result : JSON.stringify(task.result, null, 2))
- : undefined;
-
- return (
-
-
setIsExpanded(!isExpanded)}
- >
-
-
-
- 运行 #{task.runId.slice(0, 8)}
-
-
-
-
- {config.label}
-
-
-
-
-
- {/* 展开详情 */}
- {isExpanded && (
-
-
- 运行 ID
- {task.runId}
-
-
- 开始时间
- {new Date(task.startedAt).toLocaleString()}
-
- {task.completedAt && (
-
- 完成时间
- {new Date(task.completedAt).toLocaleString()}
-
- )}
- {resultText && (
-
- {resultText}
-
- )}
- {task.error && (
-
- {task.error}
-
- )}
-
- )}
-
- );
-}
-
-export default HandTaskPanel;
diff --git a/desktop/src/components/HandsPanel.tsx b/desktop/src/components/HandsPanel.tsx
deleted file mode 100644
index 57bc1e2..0000000
--- a/desktop/src/components/HandsPanel.tsx
+++ /dev/null
@@ -1,641 +0,0 @@
-/**
- * HandsPanel - ZCLAW Hands Management UI
- *
- * Displays available ZCLAW Hands (autonomous capability packages)
- * with detailed status, requirements, and activation controls.
- *
- * Design based on ZCLAW Dashboard v0.4.0
- */
-
-import { useState, useEffect, useCallback } from 'react';
-import { useHandStore, type Hand, type HandRequirement } from '../store/handStore';
-import { Zap, RefreshCw, ChevronRight, CheckCircle, XCircle, Loader2, AlertTriangle, Settings, Play, Clock } from 'lucide-react';
-import { BrowserHandCard } from './BrowserHand';
-import type { HandParameter } from '../types/hands';
-import { HAND_DEFINITIONS } from '../types/hands';
-import { HandParamsForm } from './HandParamsForm';
-import { ApprovalsPanel } from './ApprovalsPanel';
-import { useToast } from './ui/Toast';
-
-// === Tab Type ===
-type TabType = 'hands' | 'approvals';
-
-// === Status Badge Component ===
-
-type HandStatus = 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed';
-
-// === Parameter Validation Helper ===
-
-function validateAllParameters(
- parameters: HandParameter[],
- values: Record
-): Record {
- const errors: Record = {};
- parameters.forEach(param => {
- if (param.required) {
- const value = values[param.name];
- if (value === undefined || value === null || value === '') {
- errors[param.name] = `${param.label} is required`;
- }
- }
- });
- return errors;
-}
-
-interface StatusConfig {
- label: string;
- className: string;
- dotClass: string;
-}
-
-const STATUS_CONFIG: Record = {
- idle: {
- label: '就绪',
- className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
- dotClass: 'bg-green-500',
- },
- running: {
- label: '运行中',
- className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
- dotClass: 'bg-blue-500 animate-pulse',
- },
- needs_approval: {
- label: '待审批',
- className: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
- dotClass: 'bg-yellow-500',
- },
- error: {
- label: '错误',
- className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
- dotClass: 'bg-red-500',
- },
- unavailable: {
- label: '不可用',
- className: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
- dotClass: 'bg-gray-400',
- },
- setup_needed: {
- label: '需配置',
- className: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
- dotClass: 'bg-orange-500',
- },
-};
-
-function HandStatusBadge({ status }: { status: string }) {
- const config = STATUS_CONFIG[status as HandStatus] || STATUS_CONFIG.unavailable;
- return (
-
-
- {config.label}
-
- );
-}
-
-// === Category Badge Component ===
-
-const CATEGORY_CONFIG: Record = {
- productivity: { label: '生产力', className: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' },
- data: { label: '数据', className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400' },
- content: { label: '内容', className: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400' },
- communication: { label: '通信', className: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400' },
-};
-
-function CategoryBadge({ category }: { category?: string }) {
- if (!category) return null;
- const config = CATEGORY_CONFIG[category] || { label: category, className: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400' };
- return (
-
- {config.label}
-
- );
-}
-
-// === Requirement Item Component ===
-
-function RequirementItem({ requirement }: { requirement: HandRequirement }) {
- return (
-
-
- {requirement.met ? (
-
- ) : (
-
- )}
-
-
- {requirement.description}
- {requirement.details && (
- ({requirement.details})
- )}
-
-
- );
-}
-
-// === Hand Details Modal Component ===
-
-interface HandDetailsModalProps {
- hand: Hand;
- isOpen: boolean;
- onClose: () => void;
- onActivate: (params?: Record) => void;
- isActivating: boolean;
-}
-
-function HandDetailsModal({ hand, isOpen, onClose, onActivate, isActivating }: HandDetailsModalProps) {
- // Get Hand parameters from definitions
- const handDefinition = HAND_DEFINITIONS.find(h => h.id === hand.id);
- const parameters: HandParameter[] = handDefinition?.parameters || [];
-
- // Form state for parameters
- const [paramValues, setParamValues] = useState>({});
- const [paramErrors, setParamErrors] = useState>({});
- const [showParamsForm, setShowParamsForm] = useState(false);
-
- // Initialize default values
- useEffect(() => {
- if (parameters.length > 0) {
- const defaults: Record = {};
- parameters.forEach(p => {
- if (p.defaultValue !== undefined) {
- defaults[p.name] = p.defaultValue;
- }
- });
- setParamValues(defaults);
- }
- }, [parameters]);
-
- // Reset form when modal opens/closes
- useEffect(() => {
- if (isOpen) {
- setShowParamsForm(false);
- setParamErrors({});
- }
- }, [isOpen]);
-
- const handleActivateClick = useCallback(() => {
- if (parameters.length > 0 && !showParamsForm) {
- // Show params form first
- setShowParamsForm(true);
- return;
- }
-
- // Validate parameters if showing form
- if (showParamsForm) {
- const errors = validateAllParameters(parameters, paramValues);
- setParamErrors(errors);
- if (Object.keys(errors).length > 0) {
- return;
- }
- // Pass parameters to onActivate
- onActivate(paramValues);
- } else {
- onActivate();
- }
- }, [parameters, showParamsForm, paramValues, onActivate]);
-
- if (!isOpen) return null;
-
- const canActivate = hand.status === 'idle' || hand.status === 'setup_needed';
- const hasUnmetRequirements = hand.requirements?.some(r => !r.met);
-
- return (
-
- {/* Backdrop */}
-
-
- {/* Modal */}
-
- {/* Header */}
-
-
-
{hand.icon || '🤖'}
-
-
{hand.name}
-
-
-
-
-
-
- {/* Body */}
-
- {/* Description */}
-
{hand.description}
-
- {/* Agent Config */}
- {(hand.provider || hand.model) && (
-
-
- 代理配置
-
-
- {hand.provider && (
-
-
提供商
-
{hand.provider}
-
- )}
- {hand.model && (
-
- )}
-
-
- )}
-
- {/* Requirements */}
- {hand.requirements && hand.requirements.length > 0 && (
-
-
- 环境要求
-
-
- {hand.requirements.map((req, idx) => (
-
- ))}
-
-
- )}
-
- {/* Tools */}
- {hand.tools && hand.tools.length > 0 && (
-
-
- 工具 ({hand.tools.length})
-
-
- {hand.tools.map((tool, idx) => (
-
- {tool}
-
- ))}
-
-
- )}
-
- {/* Parameters Form (shown when activating) */}
- {showParamsForm && parameters.length > 0 && (
-
-
- 执行参数
-
-
-
- )}
-
- {/* Dashboard Metrics */}
- {hand.metrics && hand.metrics.length > 0 && (
-
-
- 仪表盘指标 ({hand.metrics.length})
-
-
- {hand.metrics.map((metric, idx) => (
-
- ))}
-
-
- )}
-
-
- {/* Footer */}
-
-
-
-
-
-
- );
-}
-
-// === Hand Card Component ===
-
-interface HandCardProps {
- hand: Hand;
- onDetails: (hand: Hand) => void;
- onActivate: (hand: Hand, params?: Record) => void;
- isActivating: boolean;
-}
-
-function HandCard({ hand, onDetails, onActivate, isActivating }: HandCardProps) {
- const canActivate = hand.status === 'idle';
- const hasUnmetRequirements = hand.requirements_met === false;
-
- return (
-
- {/* Header */}
-
-
- {hand.icon || '🤖'}
-
{hand.name}
-
-
-
-
- {/* Description */}
-
{hand.description}
-
- {/* Requirements Summary (if any unmet) */}
- {hasUnmetRequirements && (
-
- )}
-
- {/* Meta Info */}
-
- {hand.toolCount !== undefined && (
- {hand.toolCount} 个工具
- )}
- {hand.metricCount !== undefined && (
- {hand.metricCount} 个指标
- )}
- {hand.category && (
-
- )}
-
-
- {/* Actions */}
-
-
-
-
-
- );
-}
-
-// === Main HandsPanel Component ===
-
-export function HandsPanel() {
- const { hands, loadHands, triggerHand, isLoading, error: storeError, getHandDetails } = useHandStore();
- const [selectedHand, setSelectedHand] = useState(null);
- const [activatingHandId, setActivatingHandId] = useState(null);
- const [showModal, setShowModal] = useState(false);
- const [activeTab, setActiveTab] = useState('hands');
- const { toast } = useToast();
-
- useEffect(() => {
- loadHands();
- }, [loadHands]);
-
- const handleDetails = useCallback(async (hand: Hand) => {
- // Load full details before showing modal
- const details = await getHandDetails(hand.id);
- setSelectedHand(details || hand);
- setShowModal(true);
- }, [getHandDetails]);
-
- const handleActivate = useCallback(async (hand: Hand, params?: Record) => {
- setActivatingHandId(hand.id);
-
- try {
- const result = await triggerHand(hand.id, params);
-
- if (result) {
- toast(`Hand "${hand.name}" 已成功激活`, 'success');
- // Refresh hands after activation
- await loadHands();
- } else {
- // Check if there's an error in the store
- const errorMsg = storeError || '激活失败,请检查后端连接';
- console.error(`[HandsPanel] Hand activation failed:`, errorMsg);
- toast(`Hand "${hand.name}" 激活失败: ${errorMsg}`, 'error');
- }
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : String(err);
- console.error(`[HandsPanel] Hand activation error:`, errorMsg);
- toast(`Hand "${hand.name}" 激活异常: ${errorMsg}`, 'error');
- } finally {
- setActivatingHandId(null);
- }
- }, [triggerHand, loadHands, toast, storeError]);
-
- const handleCloseModal = useCallback(() => {
- setShowModal(false);
- setSelectedHand(null);
- }, []);
-
- const handleModalActivate = useCallback(async (params?: Record) => {
- if (!selectedHand) return;
- setShowModal(false);
- await handleActivate(selectedHand, params);
- }, [selectedHand, handleActivate]);
-
- if (isLoading && hands.length === 0) {
- return (
-
- );
- }
-
- if (hands.length === 0) {
- return (
-
-
-
-
-
暂无可用的 Hands
-
- 请连接到 ZCLAW 以查看可用的自主能力包。
-
-
- );
- }
-
- return (
-
- {/* Header */}
-
-
-
- Hands
-
-
- 自主能力包
-
-
-
-
-
- {/* Tabs */}
-
-
-
-
-
- {/* Tab Content */}
- {activeTab === 'approvals' ? (
-
- ) : (
- <>
- {/* Stats */}
-
-
- 可用 {hands.length}
-
-
- 就绪 {hands.filter(h => h.status === 'idle').length}
-
-
-
- {/* Hand Cards Grid */}
-
- {hands.map((hand) => {
- // Check if this is a Browser Hand
- const isBrowserHand = hand.id === 'browser' || hand.name === 'Browser' || hand.name?.toLowerCase().includes('browser');
-
- return isBrowserHand ? (
-
- ) : (
-
- );
- })}
-
-
- {/* Details Modal */}
- {selectedHand && (
-
- )}
- >
- )}
-
- );
-}
-
-export default HandsPanel;
diff --git a/desktop/src/components/PipelineResultPreview.tsx b/desktop/src/components/PipelineResultPreview.tsx
deleted file mode 100644
index 03cb8ec..0000000
--- a/desktop/src/components/PipelineResultPreview.tsx
+++ /dev/null
@@ -1,534 +0,0 @@
-/**
- * PipelineResultPreview - Pipeline 执行结果预览组件
- *
- * 展示 Pipeline 执行完成后的结果,支持多种预览模式:
- * - JSON 数据预览
- * - Markdown 渲染
- * - 文件下载列表
- * - 课堂预览器(特定 Pipeline)
- */
-
-import { useState } from 'react';
-import {
- FileText,
- Download,
- ExternalLink,
- Copy,
- Check,
- Code,
- File,
- Presentation,
- FileSpreadsheet,
- X,
-} from 'lucide-react';
-import { PipelineRunResponse } from '../lib/pipeline-client';
-import { useToast } from './ui/Toast';
-import DOMPurify from 'dompurify';
-import { ClassroomPreviewer, type ClassroomData } from './ClassroomPreviewer';
-import { useClassroomStore } from '../store/classroomStore';
-import { adaptToClassroom } from '../lib/classroom-adapter';
-
-// === Types ===
-
-interface PipelineResultPreviewProps {
- result: PipelineRunResponse;
- pipelineId: string;
- onClose?: () => void;
-}
-
-type PreviewMode = 'auto' | 'json' | 'markdown' | 'classroom' | 'files';
-
-// === Utility Functions ===
-
-function getFileIcon(filename: string): React.ReactNode {
- const ext = filename.split('.').pop()?.toLowerCase();
- switch (ext) {
- case 'pptx':
- case 'ppt':
- return ;
- case 'xlsx':
- case 'xls':
- return ;
- case 'pdf':
- return ;
- case 'html':
- return ;
- case 'md':
- case 'markdown':
- return ;
- default:
- return ;
- }
-}
-
-function formatFileSize(bytes: number): string {
- if (bytes < 1024) return `${bytes} B`;
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
-}
-
-// === Sub-Components ===
-
-interface FileDownloadCardProps {
- file: {
- name: string;
- url: string;
- size?: number;
- };
-}
-
-function FileDownloadCard({ file }: FileDownloadCardProps) {
- const handleDownload = () => {
- // Create download link
- const link = document.createElement('a');
- link.href = file.url;
- link.download = file.name;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- };
-
- return (
-
- {getFileIcon(file.name)}
-
-
- {file.name}
-
- {file.size && (
-
- {formatFileSize(file.size)}
-
- )}
-
-
-
-
-
-
- );
-}
-
-interface JsonPreviewProps {
- data: unknown;
-}
-
-function JsonPreview({ data }: JsonPreviewProps) {
- const [copied, setCopied] = useState(false);
- const { toast } = useToast();
-
- const jsonString = JSON.stringify(data, null, 2);
-
- const handleCopy = async () => {
- await navigator.clipboard.writeText(jsonString);
- setCopied(true);
- toast('已复制到剪贴板', 'success');
- setTimeout(() => setCopied(false), 2000);
- };
-
- return (
-
-
-
- {jsonString}
-
-
- );
-}
-
-interface MarkdownPreviewProps {
- content: string;
-}
-
-function MarkdownPreview({ content }: MarkdownPreviewProps) {
- // Simple markdown rendering with XSS protection
- const renderMarkdown = (md: string): string => {
- // First, escape HTML entities to prevent XSS
- const escaped = md
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-
- return escaped
- // Headers
- .replace(/^### (.*$)/gim, '$1
')
- .replace(/^## (.*$)/gim, '$1
')
- .replace(/^# (.*$)/gim, '$1
')
- // Bold
- .replace(/\*\*(.*?)\*\*/g, '$1')
- // Italic
- .replace(/\*(.*?)\*/g, '$1')
- // Lists
- .replace(/^- (.*$)/gim, '$1')
- // Paragraphs
- .replace(/\n\n/g, '
')
- // Line breaks
- .replace(/\n/g, '
');
- };
-
- return (
-
- );
-}
-
-// === Main Component ===
-
-export function PipelineResultPreview({
- result,
- pipelineId,
- onClose,
-}: PipelineResultPreviewProps) {
- const [mode, setMode] = useState('auto');
- const { toast } = useToast();
-
- // Determine the best preview mode
- const outputs = result.outputs as Record | undefined;
- const exportFiles = (outputs?.export_files as Array<{ name: string; url: string; size?: number }>) || [];
-
- // Check if this is a classroom pipeline
- const isClassroom = pipelineId === 'classroom-generator' || pipelineId.includes('classroom');
-
- // Auto-detect preview mode
- const autoMode: PreviewMode = isClassroom ? 'classroom' :
- exportFiles.length > 0 ? 'files' :
- typeof outputs === 'object' ? 'json' : 'json';
-
- const activeMode = mode === 'auto' ? autoMode : mode;
-
- // Handle classroom export
- const handleClassroomExport = (format: 'pptx' | 'html' | 'pdf', data: ClassroomData) => {
- toast(`正在导出 ${format.toUpperCase()} 格式...`, 'info');
-
- // Create downloadable content based on format
- setTimeout(() => {
- try {
- if (format === 'html') {
- // Generate HTML content
- const htmlContent = generateClassroomHTML(data);
- downloadFile(htmlContent, `${data.title}.html`, 'text/html');
- toast('HTML 导出成功', 'success');
- } else if (format === 'pptx') {
- // For PPTX, we would need a library like pptxgenjs
- // For now, export as JSON that can be converted
- const pptxData = JSON.stringify(data, null, 2);
- downloadFile(pptxData, `${data.title}.slides.json`, 'application/json');
- toast('幻灯片数据已导出(JSON格式)', 'success');
- } else if (format === 'pdf') {
- // For PDF, we would need a library like jspdf
- // For now, export as printable HTML
- const htmlContent = generatePrintableHTML(data);
- const printWindow = window.open('', '_blank');
- if (printWindow) {
- printWindow.document.write(htmlContent);
- printWindow.document.close();
- printWindow.print();
- toast('已打开打印预览', 'success');
- }
- }
- } catch (err) {
- const errorMsg = err instanceof Error ? err.message : '导出失败';
- toast(`导出失败: ${errorMsg}`, 'error');
- }
- }, 500);
- };
-
- // Render based on mode
- const renderContent = () => {
- switch (activeMode) {
- case 'json':
- return ;
-
- case 'markdown': {
- const mdContent = (outputs?.summary || outputs?.report || JSON.stringify(outputs, null, 2)) as string;
- return ;
- }
-
- case 'classroom': {
- // Convert outputs to ClassroomData format
- const classroomData: ClassroomData | null = outputs ? {
- id: result.pipelineId || 'classroom',
- title: (outputs.title as string) || '课堂内容',
- subject: (outputs.subject as string) || '通用',
- difficulty: (outputs.difficulty as '初级' | '中级' | '高级') || '中级',
- duration: (outputs.duration as number) || 30,
- scenes: Array.isArray(outputs.scenes) ? (outputs.scenes as ClassroomData['scenes']) : [],
- outline: (outputs.outline as ClassroomData['outline']) || { sections: [] },
- createdAt: new Date().toISOString(),
- } : null;
-
- if (classroomData && classroomData.scenes.length > 0) {
- return (
-
- {
- // Handle export
- handleClassroomExport(format, classroomData);
- }}
- onOpenFullPlayer={() => {
- const classroom = adaptToClassroom(classroomData);
- useClassroomStore.getState().setActiveClassroom(classroom);
- useClassroomStore.getState().openClassroom();
- }}
- />
-
- );
- }
-
- return (
-
-
-
无法解析课堂数据
-
您可以在下方下载生成的文件
-
- );
- }
-
- default:
- return ;
- }
- };
-
- return (
-
- {/* Header */}
-
-
-
- Pipeline 执行完成
-
-
- {result.pipelineId} · {result.status === 'completed' ? '成功' : result.status}
-
-
- {onClose && (
-
- )}
-
-
- {/* Mode Tabs */}
-
-
-
-
- {isClassroom && (
-
- )}
-
-
- {/* Content */}
-
- {renderContent()}
-
-
- {/* Export Files */}
- {exportFiles.length > 0 && (
-
-
- 导出文件 ({exportFiles.length})
-
-
- {exportFiles.map((file, index) => (
-
- ))}
-
-
- )}
-
- {/* Footer */}
-
-
- 执行时间: {new Date(result.startedAt).toLocaleString()}
-
- {onClose && (
-
- )}
-
-
- );
-}
-
-export default PipelineResultPreview;
-
-// === Helper Functions ===
-
-function downloadFile(content: string, filename: string, mimeType: string) {
- const blob = new Blob([content], { type: mimeType });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
-}
-
-function generateClassroomHTML(data: ClassroomData): string {
- const scenesHTML = data.scenes.map((scene, index) => `
-
-
-
${scene.content.heading || scene.title}
- ${scene.content.bullets ? `
-
- ${scene.content.bullets.map(b => `- ${b}
`).join('')}
-
- ` : ''}
- ${scene.narration ? `
${scene.narration}
` : ''}
-
-
- `).join('');
-
- return `
-
-
-
-
- ${data.title}
-
-
-
-
-
-
- ${scenesHTML}
-
-
-
-
-`;
-}
-
-function generatePrintableHTML(data: ClassroomData): string {
- return generateClassroomHTML(data);
-}
diff --git a/desktop/src/components/PipelinesPanel.tsx b/desktop/src/components/PipelinesPanel.tsx
deleted file mode 100644
index 7c92fd7..0000000
--- a/desktop/src/components/PipelinesPanel.tsx
+++ /dev/null
@@ -1,567 +0,0 @@
-/**
- * PipelinesPanel - Pipeline Discovery and Execution UI
- *
- * Displays available Pipelines (DSL-based workflows) with
- * category filtering, search, and execution capabilities.
- *
- * Pipelines orchestrate Skills and Hands to accomplish complex tasks.
- */
-
-import { useState, useEffect } from 'react';
-import {
- Play,
- RefreshCw,
- Search,
- Loader2,
- XCircle,
- Package,
- Filter,
- X,
-} from 'lucide-react';
-import { PipelineResultPreview } from './PipelineResultPreview';
-import {
- PipelineClient,
- PipelineInfo,
- PipelineRunResponse,
- usePipelines,
- validateInputs,
- getDefaultForType,
- formatInputType,
-} from '../lib/pipeline-client';
-import { useToast } from './ui/Toast';
-import { saasClient } from '../lib/saas-client';
-
-// === Category Badge Component ===
-
-const CATEGORY_CONFIG: Record = {
- education: { label: '教育', className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' },
- marketing: { label: '营销', className: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' },
- legal: { label: '法律', className: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400' },
- productivity: { label: '生产力', className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' },
- research: { label: '研究', className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400' },
- sales: { label: '销售', className: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400' },
- hr: { label: '人力', className: 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400' },
- finance: { label: '财务', className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' },
- default: { label: '其他', className: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400' },
-};
-
-
-function CategoryBadge({ category }: { category: string }) {
- const config = CATEGORY_CONFIG[category] || CATEGORY_CONFIG.default;
- return (
-
- {config.label}
-
- );
-}
-
-// === Pipeline Card Component ===
-
-interface PipelineCardProps {
- pipeline: PipelineInfo;
- onRun: (pipeline: PipelineInfo) => void;
-}
-
-function PipelineCard({ pipeline, onRun }: PipelineCardProps) {
- return (
-
-
-
-
{pipeline.icon}
-
-
- {pipeline.displayName}
-
-
- {pipeline.id} · v{pipeline.version}
-
-
-
-
-
-
-
- {pipeline.description}
-
-
- {pipeline.tags.length > 0 && (
-
- {pipeline.tags.slice(0, 3).map((tag) => (
-
- {tag}
-
- ))}
- {pipeline.tags.length > 3 && (
-
- +{pipeline.tags.length - 3}
-
- )}
-
- )}
-
-
-
- {pipeline.inputs.length} 个输入参数
-
-
-
-
- );
-}
-
-// === Pipeline Run Modal ===
-
-interface RunModalProps {
- pipeline: PipelineInfo;
- onClose: () => void;
- onComplete: (result: PipelineRunResponse) => void;
-}
-
-function RunModal({ pipeline, onClose, onComplete }: RunModalProps) {
- const [values, setValues] = useState>(() => {
- const defaults: Record = {};
- pipeline.inputs.forEach((input) => {
- defaults[input.name] = input.default ?? getDefaultForType(input.inputType);
- });
- return defaults;
- });
- const [errors, setErrors] = useState([]);
- const [running, setRunning] = useState(false);
- const [progress, setProgress] = useState(null);
-
- const handleInputChange = (name: string, value: unknown) => {
- setValues((prev) => ({ ...prev, [name]: value }));
- setErrors([]);
- };
-
- const handleRun = async () => {
- // Validate inputs
- const validation = validateInputs(pipeline.inputs, values);
- if (!validation.valid) {
- setErrors(validation.errors);
- return;
- }
-
- setRunning(true);
- setProgress(null);
-
- try {
- const result = await PipelineClient.runAndWait(
- { pipelineId: pipeline.id, inputs: values },
- (p) => setProgress(p)
- );
-
- if (result.status === 'completed') {
- onComplete(result);
- } else if (result.error) {
- setErrors([result.error]);
- }
- } catch (err) {
- setErrors([err instanceof Error ? err.message : String(err)]);
- } finally {
- setRunning(false);
- }
- };
-
- const renderInput = (input: typeof pipeline.inputs[0]) => {
- const value = values[input.name];
-
- switch (input.inputType) {
- case 'string':
- case 'text':
- return input.inputType === 'text' ? (
-