Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
DeerFlow frontend visual overhaul: - Card-style input box (white rounded card, textarea top, actions bottom) - Dropdown mode selector (闪速/思考/Pro/Ultra with icons+descriptions) - Colored quick-action chips (小惊喜/写作/研究/收集/学习) - Minimal top bar (title + token count + export) - Warm gray color system (#faf9f6 bg, #f5f4f1 sidebar, #e8e6e1 border) - DeerFlow-style sidebar (新对话/对话/智能体 nav) - Reasoning block, tool call chain, task progress visualization - Streaming text, model selector, suggestion chips components - Resizable artifact panel with drag handle - Virtualized message list for 100+ messages Bug fixes: - Stream hang: GatewayClient onclose code 1000 now calls onComplete - WebView2 textarea border: CSS !important override for UA styles - Gateway stream event handling (response/phase/tool_call types) Intelligence client: - Unified client with fallback drivers (compactor/heartbeat/identity/memory/reflection) - Gateway API types and type conversions
170 lines
5.7 KiB
TypeScript
170 lines
5.7 KiB
TypeScript
import { useState, createContext, useContext, useCallback, type ReactNode } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { ChevronRight, CheckCircle2, XCircle, Loader2, Circle } from 'lucide-react';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TaskContext — shared task state for sub-agent orchestration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface TaskContextValue {
|
|
tasks: Subtask[];
|
|
updateTask: (id: string, updates: Partial<Subtask>) => void;
|
|
}
|
|
|
|
const TaskContext = createContext<TaskContextValue | null>(null);
|
|
|
|
export function useTaskContext() {
|
|
const ctx = useContext(TaskContext);
|
|
if (!ctx) {
|
|
throw new Error('useTaskContext must be used within TaskProvider');
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export function TaskProvider({
|
|
children,
|
|
initialTasks = [],
|
|
}: {
|
|
children: ReactNode;
|
|
initialTasks?: Subtask[];
|
|
}) {
|
|
const [tasks, setTasks] = useState<Subtask[]>(initialTasks);
|
|
|
|
const updateTask = useCallback((id: string, updates: Partial<Subtask>) => {
|
|
setTasks(prev => prev.map(t => (t.id === id ? { ...t, ...updates } : t)));
|
|
}, []);
|
|
|
|
return (
|
|
<TaskContext.Provider value={{ tasks, updateTask }}>
|
|
{children}
|
|
</TaskContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Subtask progress display for sub-agent orchestration.
|
|
*
|
|
* Inspired by DeerFlow's SubtaskCard + ShineBorder pattern:
|
|
* - Shows task status with animated indicators
|
|
* - Collapsible details with thinking chain
|
|
* - Pulsing border animation for active tasks
|
|
* - Status icons: running (pulse), completed (green), failed (red)
|
|
*/
|
|
|
|
export interface Subtask {
|
|
id: string;
|
|
description: string;
|
|
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
result?: string;
|
|
error?: string;
|
|
steps?: Array<{ content: string; status: 'thinking' | 'done' | 'error' }>;
|
|
}
|
|
|
|
interface TaskProgressProps {
|
|
tasks: Subtask[];
|
|
className?: string;
|
|
}
|
|
|
|
export function TaskProgress({ tasks, className = '' }: TaskProgressProps) {
|
|
if (tasks.length === 0) return null;
|
|
|
|
return (
|
|
<div className={`space-y-2 ${className}`}>
|
|
{tasks.map(task => (
|
|
<SubtaskCard key={task.id} task={task} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SubtaskCard({ task }: { task: Subtask }) {
|
|
const [expanded, setExpanded] = useState(task.status === 'in_progress');
|
|
const isActive = task.status === 'in_progress';
|
|
|
|
return (
|
|
<div
|
|
className={`
|
|
rounded-lg border transition-all overflow-hidden
|
|
${isActive
|
|
? 'border-orange-300 dark:border-orange-700 bg-orange-50/50 dark:bg-orange-900/10 shadow-[0_0_15px_-3px_rgba(249,115,22,0.15)] dark:shadow-[0_0_15px_-3px_rgba(249,115,22,0.1)]'
|
|
: task.status === 'completed'
|
|
? 'border-green-200 dark:border-green-800 bg-green-50/30 dark:bg-green-900/10'
|
|
: task.status === 'failed'
|
|
? 'border-red-200 dark:border-red-800 bg-red-50/30 dark:bg-red-900/10'
|
|
: 'border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50'
|
|
}
|
|
`}
|
|
>
|
|
{/* Header */}
|
|
<button
|
|
onClick={() => setExpanded(!expanded)}
|
|
className="w-full flex items-center gap-2 px-3 py-2 text-left"
|
|
>
|
|
<motion.span animate={{ rotate: expanded ? 90 : 0 }} transition={{ duration: 0.15 }}>
|
|
<ChevronRight className="w-3.5 h-3.5 text-gray-400" />
|
|
</motion.span>
|
|
|
|
{/* Status icon */}
|
|
{task.status === 'in_progress' ? (
|
|
<Loader2 className="w-4 h-4 text-orange-500 animate-spin" />
|
|
) : task.status === 'completed' ? (
|
|
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
|
) : task.status === 'failed' ? (
|
|
<XCircle className="w-4 h-4 text-red-500" />
|
|
) : (
|
|
<Circle className="w-4 h-4 text-gray-400" />
|
|
)}
|
|
|
|
<span className="flex-1 text-xs font-medium text-gray-700 dark:text-gray-300 truncate">
|
|
{task.description}
|
|
</span>
|
|
</button>
|
|
|
|
{/* Expanded details */}
|
|
<AnimatePresence>
|
|
{expanded && (task.result || task.error || (task.steps && task.steps.length > 0)) && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="px-3 pb-2 ml-6 border-l-2 border-gray-200 dark:border-gray-700 space-y-1">
|
|
{/* Steps */}
|
|
{task.steps?.map((step, i) => (
|
|
<div key={i} className="flex items-start gap-2">
|
|
{step.status === 'thinking' ? (
|
|
<span className="w-1.5 h-1.5 mt-1.5 bg-amber-400 rounded-full animate-pulse flex-shrink-0" />
|
|
) : step.status === 'done' ? (
|
|
<span className="w-1.5 h-1.5 mt-1.5 bg-green-500 rounded-full flex-shrink-0" />
|
|
) : (
|
|
<span className="w-1.5 h-1.5 mt-1.5 bg-red-500 rounded-full flex-shrink-0" />
|
|
)}
|
|
<span className="text-[11px] text-gray-600 dark:text-gray-400 leading-relaxed">
|
|
{step.content}
|
|
</span>
|
|
</div>
|
|
))}
|
|
|
|
{/* Result */}
|
|
{task.result && (
|
|
<div className="text-xs text-gray-700 dark:text-gray-300 mt-1 whitespace-pre-wrap">
|
|
{task.result}
|
|
</div>
|
|
)}
|
|
|
|
{/* Error */}
|
|
{task.error && (
|
|
<div className="text-xs text-red-600 dark:text-red-400 mt-1">
|
|
{task.error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
}
|