feat(desktop): DeerFlow visual redesign + stream hang fix + intelligence client
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
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
This commit is contained in:
169
desktop/src/components/ai/TaskProgress.tsx
Normal file
169
desktop/src/components/ai/TaskProgress.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user