/** * TaskQueuePanel Component * * Side panel that displays all tasks in the queue. * Supports filtering by status and provides batch operations. */ import React, { useState, useMemo } from 'react'; import { Button, Tabs, Dialog, MessagePlugin, Input, Radio } from 'tdesign-react'; import { DeleteIcon, SearchIcon } from 'tdesign-icons-react'; import { TaskItem } from './TaskItem'; import { useTaskQueue } from '../../hooks/useTaskQueue'; import { Task, TaskType } from '../../types/task.types'; import { useDrawnix } from '../../hooks/use-drawnix'; import { insertImageFromUrl } from '../../data/image'; import { insertVideoFromUrl } from '../../data/video'; import './task-queue.scss'; const { TabPanel } = Tabs; const RadioGroup = Radio.Group; export interface TaskQueuePanelProps { /** Whether the panel is expanded */ expanded: boolean; /** Callback when close button is clicked */ onClose?: () => void; /** Callback when a task action is performed */ onTaskAction?: (action: string, taskId: string) => void; } /** * TaskQueuePanel component - displays the full task queue */ export const TaskQueuePanel: React.FC = ({ expanded, onClose, onTaskAction, }) => { const { tasks, activeTasks, completedTasks, failedTasks, cancelledTasks, cancelTask, retryTask, deleteTask, clearCompleted, clearFailed, } = useTaskQueue(); const { board } = useDrawnix(); const [activeTab, setActiveTab] = useState('active'); const [showClearConfirm, setShowClearConfirm] = useState(false); const [clearType, setClearType] = useState<'completed' | 'failed'>('completed'); const [searchText, setSearchText] = useState(''); const [typeFilter, setTypeFilter] = useState<'all' | 'image' | 'video'>('all'); // Filter and sort tasks const filteredTasks = useMemo(() => { // Get tasks based on active tab let tasksToFilter: Task[]; switch (activeTab) { case 'all': tasksToFilter = tasks; break; case 'active': tasksToFilter = activeTasks; break; case 'completed': tasksToFilter = completedTasks; break; case 'failed': tasksToFilter = failedTasks; break; case 'cancelled': tasksToFilter = cancelledTasks; break; default: tasksToFilter = tasks; } // Apply type filter if (typeFilter !== 'all') { tasksToFilter = tasksToFilter.filter(task => task.type === (typeFilter === 'image' ? TaskType.IMAGE : TaskType.VIDEO) ); } // Apply search filter if (searchText.trim()) { const searchLower = searchText.toLowerCase().trim(); tasksToFilter = tasksToFilter.filter(task => task.params.prompt.toLowerCase().includes(searchLower) ); } // Sort by time - newest first (reverse chronological) return [...tasksToFilter].sort((a, b) => b.createdAt - a.createdAt); }, [activeTab, tasks, activeTasks, completedTasks, failedTasks, cancelledTasks, typeFilter, searchText]); // Handle clear action const handleClear = (type: 'completed' | 'failed') => { setClearType(type); setShowClearConfirm(true); }; const confirmClear = () => { if (clearType === 'completed') { clearCompleted(); } else { clearFailed(); } setShowClearConfirm(false); }; // Task action handlers const handleCancel = (taskId: string) => { cancelTask(taskId); onTaskAction?.('cancel', taskId); }; const handleRetry = (taskId: string) => { retryTask(taskId); onTaskAction?.('retry', taskId); }; const handleDelete = (taskId: string) => { deleteTask(taskId); onTaskAction?.('delete', taskId); }; const handleDownload = async (taskId: string) => { const task = tasks.find(t => t.id === taskId); if (!task?.result?.url) return; try { // Fetch the file as blob to handle cross-origin URLs const response = await fetch(task.result.url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); // Create blob URL const blobUrl = URL.createObjectURL(blob); // Generate filename from prompt (sanitize and truncate) const sanitizedPrompt = task.params.prompt .replace(/[^a-zA-Z0-9\u4e00-\u9fa5\s-]/g, '') // Remove special chars, keep Chinese .replace(/\s+/g, '-') // Replace spaces with dashes .substring(0, 50); // Limit to 50 chars const filename = `${sanitizedPrompt || task.type}.${task.result.format}`; // Create a temporary link to download the file const link = document.createElement('a'); link.href = blobUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up blob URL URL.revokeObjectURL(blobUrl); MessagePlugin.success('下载成功'); onTaskAction?.('download', taskId); } catch (error) { console.error('Download failed:', error); MessagePlugin.error(`下载失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; const handleInsert = async (taskId: string) => { const task = tasks.find(t => t.id === taskId); if (!task?.result?.url || !board) { console.warn('Cannot insert: task result or board not available'); MessagePlugin.warning('无法插入:白板未就绪'); return; } try { if (task.type === TaskType.IMAGE) { // 插入图片到白板 await insertImageFromUrl(board, task.result.url); console.log('Image inserted to board:', taskId); MessagePlugin.success('图片已插入到白板'); } else if (task.type === TaskType.VIDEO) { // 插入视频到白板 await insertVideoFromUrl(board, task.result.url); console.log('Video inserted to board:', taskId); MessagePlugin.success('视频已插入到白板'); } onTaskAction?.('insert', taskId); } catch (error) { console.error('Failed to insert to board:', error); MessagePlugin.error(`插入失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; return ( <>
{/* Header with title and tabs */}

任务队列

setActiveTab(value as string)}>
{/* Filters and Actions */}
setSearchText(value)} placeholder="搜索 Prompt..." clearable prefixIcon={} size="small" style={{ flex: 1, marginRight: '8px' }} /> setTypeFilter(value as 'all' | 'image' | 'video')} size="small" variant="default-filled" style={{ marginRight: '8px' }} > 全部 图片 视频
{completedTasks.length > 0 && ( )} {failedTasks.length > 0 && ( )}
{/* Task List */}
{filteredTasks.length === 0 ? (
📋
{activeTab === 'all' ? '暂无任务' : `暂无${activeTab === 'active' ? '活动' : activeTab === 'completed' ? '已完成' : activeTab === 'failed' ? '失败' : '已取消'}任务`}
) : (
{filteredTasks.map(task => ( ))}
)}
{/* Backdrop overlay */} {expanded && (
)} {/* Clear Confirmation Dialog */} setShowClearConfirm(false)} onConfirm={confirmClear} onCancel={() => setShowClearConfirm(false)} > 确定要清除所有{clearType === 'completed' ? '已完成' : '失败'}的任务吗?此操作无法撤销。 ); };