UserTasksDebuggerTab.tsx31.1 KBView on GitHub import { Check, Copy, ChevronDown, ChevronRight, Search, Calendar } from 'lucide-react';
import type { TaskDateKey } from '@/modules/userTasks/slice/userTasksSlice';
import { motion, AnimatePresence } from 'motion/react';
import React, { useState, useMemo } from 'react';
import { useCedarStore } from '@/modules/store';
import { cn } from '@/styles/stylingUtils';
import { JsonTreeView } from './JsonTreeView';
interface UserTasksTabProps {
tasks: Record<string, any>;
scheduledExecutions: Record<string, any>;
onCopy: (text: string, id: string) => void;
copiedId: string | null;
}
// Date key labels for display
const DATE_KEY_LABELS: Record<TaskDateKey, string> = {
previous: 'Previous (< -3 days)',
'-3': '3 Days Ago',
'-2': '2 Days Ago',
'-1': 'Yesterday',
today: 'Today',
'+1': 'Tomorrow',
'+2': 'In 2 Days',
'+3': 'In 3 Days',
future: 'Future (> +3 days)',
};
const DATE_KEY_ORDER: TaskDateKey[] = [
'previous',
'-3',
'-2',
'-1',
'today',
'+1',
'+2',
'+3',
'future',
];
// Component to display tasks organized by date
const TasksByDateComponent = ({
onCopy,
copiedId,
}: {
onCopy: (text: string, id: string) => void;
copiedId: string | null;
}) => {
const taskIdsByDateKey=[redacted] => state.taskIdsByDateKey);
const executionIdsByDateKey=[redacted] => state.executionIdsByDateKey);
const tasksViewSelectedDate = useCedarStore((state) => state.tasksViewSelectedDate);
const tasks = useCedarStore((state) => state.tasks);
const scheduledExecutions = useCedarStore((state) => state.scheduledExecutions);
const [isSectionExpanded, setIsSectionExpanded] = useState(true);
const [expandedDateKeys, setExpandedDateKeys] = useState<Set<TaskDateKey>>(new Set(['today']));
const [expandedTasks, setExpandedTasks] = useState<Set<string>>(new Set());
const safeStringify = (obj: unknown, indent = 2): string => {
try {
return JSON.stringify(obj, null, indent);
} catch (error) {
return `[Error serializing object: ${error instanceof Error ? error.message : 'Unknown error'}]`;
}
};
const toggleDateKey=[redacted] TaskDateKey) => {
setExpandedDateKeys((prev) => {
const next = new Set(prev);
if (next.has(dateKey)) {
next.delete(dateKey);
} else {
next.add(dateKey);
}
return next;
});
};
const toggleTask = (taskId: string) => {
setExpandedTasks((prev) => {
const next = new Set(prev);
if (next.has(taskId)) {
next.delete(taskId);
} else {
next.add(taskId);
}
return next;
});
};
// Calculate total items across all date keys
const totalTasks = DATE_KEY_ORDER.reduce(
(sum, key) => sum + (taskIdsByDateKey[key]?.length || 0),
0,
);
const totalExecutions = DATE_KEY_ORDER.reduce(
(sum, key) => sum + (executionIdsByDateKey[key]?.length || 0),
0,
);
// Get selected date key based on tasksViewSelectedDate
const getSelectedDateKey = (): TaskDateKey | null => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const selected = new Date(tasksViewSelectedDate);
selected.setHours(0, 0, 0, 0);
const diffDays = Math.round((selected.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays < -3) return 'previous';
if (diffDays === -3) return '-3';
if (diffDays === -2) return '-2';
if (diffDays === -1) return '-1';
if (diffDays === 0) return 'today';
if (diffDays === 1) return '+1';
if (diffDays === 2) return '+2';
if (diffDays === 3) return '+3';
if (diffDays > 3) return 'future';
return null;
};
const selectedDateKey=[redacted];
return (
<div className="border-b border-gray-200 bg-blue-50/50 dark:border-gray-700 dark:bg-blue-950/20">
<div
className="flex cursor-pointer items-center justify-between p-3 transition-colors hover:bg-blue-100/50 dark:hover:bg-blue-900/30"
onClick={() => setIsSectionExpanded(!isSectionExpanded)}
>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<span className="font-medium text-blue-900 dark:text-blue-100">
Tasks By Date ({totalTasks} tasks, {totalExecutions} executions)
</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
onCopy(
safeStringify({
taskIdsByDateKey,
executionIdsByDateKey,
tasksViewSelectedDate: tasksViewSelectedDate.toISOString(),
}),
'tasksByDate',
);
}}
className="rounded p-0.5 transition-colors hover:bg-blue-200 dark:hover:bg-blue-800"
>
{copiedId === 'tasksByDate' ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
{isSectionExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</div>
</div>
<AnimatePresence>
{isSectionExpanded && (
<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="max-h-[500px] space-y-3 overflow-y-auto p-3 pt-0">
{/* Selected Date Info */}
<div className="rounded bg-white p-2 dark:bg-gray-800">
<div className="mb-1 text-xs font-semibold text-blue-700 dark:text-blue-300">
Selected Date:
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-xs">
{tasksViewSelectedDate.toLocaleDateString()}
</span>
{selectedDateKey && (
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{DATE_KEY_LABELS[selectedDateKey]}
</span>
)}
</div>
</div>
{/* Date Key Breakdown */}
<div className="space-y-2">
{DATE_KEY_ORDER.map((dateKey) => {
const taskIds = taskIdsByDateKey[dateKey] || [];
const executionIds = executionIdsByDateKey[dateKey] || [];
const isExpanded = expandedDateKeys.has(dateKey);
const isSelected = dateKey === selectedDateKey;
const hasItems = taskIds.length > 0 || executionIds.length > 0;
return (
<div
key=[redacted]
className={cn(
'rounded border',
isSelected
? 'border-blue-400 bg-blue-100 dark:border-blue-600 dark:bg-blue-900/50'
: 'border-gray-200 bg-white dark:border-gray-600 dark:bg-gray-800',
)}
>
<div
className={cn(
'flex cursor-pointer items-center justify-between p-2',
isExpanded ? 'rounded-t' : 'rounded',
isSelected
? 'hover:bg-blue-200/50 dark:hover:bg-blue-800/50'
: 'hover:bg-gray-50 dark:hover:bg-gray-700',
)}
onClick={() => toggleDateKey(dateKey)}
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<span
className={cn(
'text-xs font-medium',
isSelected ? 'text-blue-800 dark:text-blue-200' : '',
)}
>
{DATE_KEY_LABELS[dateKey]}
</span>
<span className="rounded bg-gray-200 px-1.5 py-0.5 text-xs text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{taskIds.length} tasks
</span>
{executionIds.length > 0 && (
<span className="rounded bg-orange-100 px-1.5 py-0.5 text-xs text-orange-800 dark:bg-orange-900 dark:text-orange-200">
{executionIds.length} executions
</span>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify({ taskIds, executionIds }), `dateKey-${dateKey}`);
}}
className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600"
>
{copiedId === `dateKey-${dateKey}` ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
</div>
<AnimatePresence>
{isExpanded && hasItems && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15 }}
className="overflow-hidden"
>
<div
className="space-y-2 border-t border-gray-200 p-2 dark:border-gray-600"
onClick={(e) => e.stopPropagation()}
>
{/* Tasks */}
{taskIds.map((taskId) => {
const task = tasks[taskId];
const isTaskExpanded = expandedTasks.has(taskId);
return (
<div
key=[redacted]
className="rounded border border-gray-200 bg-gray-50 dark:border-gray-600 dark:bg-gray-700"
>
<div
className={cn(
'flex cursor-pointer items-center justify-between p-1.5 hover:bg-gray-100 dark:hover:bg-gray-600',
isTaskExpanded ? 'rounded-t' : 'rounded',
)}
onClick={() => toggleTask(taskId)}
>
<div className="flex flex-1 flex-col gap-0.5">
<div className="flex items-center gap-2">
{isTaskExpanded ? (
<ChevronDown className="h-2.5 w-2.5" />
) : (
<ChevronRight className="h-2.5 w-2.5" />
)}
<span className="font-mono text-[10px]">
{taskId.slice(0, 8)}...
</span>
{task?.taskType && (
<span className="rounded bg-purple-100 px-1 py-0.5 text-[10px] text-purple-800 dark:bg-purple-900 dark:text-purple-200">
{task.taskType}
</span>
)}
{task?.taskOutput?.kind && (
<span className="rounded bg-green-100 px-1 py-0.5 text-[10px] text-green-800 dark:bg-green-900 dark:text-green-200">
{task.taskOutput.kind}
</span>
)}
</div>
{task?.description && (
<span className="ml-5 max-w-[300px] truncate text-[10px] text-gray-600 dark:text-gray-400">
{task.description}
</span>
)}
{task?.dueDate && (
<span className="ml-5 text-[10px] text-gray-500 dark:text-gray-500">
Due: {new Date(task.dueDate).toLocaleString()}
</span>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify(task), taskId);
}}
className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-500"
>
{copiedId === taskId ? (
<Check className="h-2.5 w-2.5 text-green-600" />
) : (
<Copy className="h-2.5 w-2.5" />
)}
</button>
</div>
<AnimatePresence>
{isTaskExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.1 }}
className="overflow-hidden"
>
<div
className="border-t border-gray-200 p-1.5 dark:border-gray-600"
onClick={(e) => e.stopPropagation()}
>
<JsonTreeView data={task} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-48" />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
{/* Executions */}
{executionIds.map((execId) => {
const execution = scheduledExecutions[execId];
return (
<div
key=[redacted]
className="flex items-center gap-2 rounded border border-orange-200 bg-orange-50 p-1.5 dark:border-orange-800 dark:bg-orange-900/30"
>
<span className="font-mono text-[10px]">
{execId.slice(0, 8)}...
</span>
{execution?.scheduledFor && (
<span className="text-[10px] text-orange-700 dark:text-orange-300">
{new Date(execution.scheduledFor).toLocaleString()}
</span>
)}
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify(execution), execId);
}}
className="ml-auto rounded p-0.5 hover:bg-orange-200 dark:hover:bg-orange-800"
>
{copiedId === execId ? (
<Check className="h-2.5 w-2.5 text-green-600" />
) : (
<Copy className="h-2.5 w-2.5" />
)}
</button>
</div>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export const UserTasksDebuggerTab: React.FC<UserTasksTabProps> = ({
tasks,
scheduledExecutions,
onCopy,
copiedId,
}) => {
const [expandedRawTasks, setExpandedRawTasks] = useState<Set<string>>(new Set());
const [expandedRawExecutions, setExpandedRawExecutions] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState('');
const taskEntries = useMemo(() => Object.entries(tasks), [tasks]);
const executionEntries = useMemo(
() => Object.entries(scheduledExecutions),
[scheduledExecutions],
);
const filteredTasks = useMemo(() => {
if (!searchQuery) return taskEntries;
const query = searchQuery.toLowerCase();
return taskEntries.filter(([taskId, task]) => {
if (taskId.toLowerCase().includes(query)) return true;
if (task?.conversationId?.toLowerCase().includes(query)) return true;
try {
const jsonString = JSON.stringify(task).toLowerCase();
if (jsonString.includes(query)) return true;
} catch {}
return false;
});
}, [taskEntries, searchQuery]);
const filteredExecutions = useMemo(() => {
if (!searchQuery) return executionEntries;
const query = searchQuery.toLowerCase();
return executionEntries.filter(([runId, execution]) => {
if (runId.toLowerCase().includes(query)) return true;
if (execution?.conversationId?.toLowerCase().includes(query)) return true;
if (execution?.threadId?.toLowerCase().includes(query)) return true;
try {
const jsonString = JSON.stringify(execution).toLowerCase();
if (jsonString.includes(query)) return true;
} catch {}
return false;
});
}, [executionEntries, searchQuery]);
const toggleRawTask = (taskId: string) => {
setExpandedRawTasks((prev) => {
const next = new Set(prev);
if (next.has(taskId)) {
next.delete(taskId);
} else {
next.add(taskId);
}
return next;
});
};
const toggleRawExecution = (runId: string) => {
setExpandedRawExecutions((prev) => {
const next = new Set(prev);
if (next.has(runId)) {
next.delete(runId);
} else {
next.add(runId);
}
return next;
});
};
const safeStringify = (obj: unknown, indent = 2): string => {
try {
return JSON.stringify(obj, null, indent);
} catch (error) {
return `[Error serializing: ${error instanceof Error ? error.message : 'Unknown'}]`;
}
};
return (
<div className="flex h-full flex-col">
{/* Tasks By Date Section */}
<TasksByDateComponent onCopy={onCopy} copiedId={copiedId} />
{/* Search Bar */}
<div className="border-b border-gray-200 p-2 dark:border-gray-700">
<div className="relative">
<Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Search tasks and executions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded border border-gray-300 bg-white py-1 pl-7 pr-2 text-xs placeholder:text-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
{/* Content */}
<div className="flex-1 space-y-3 overflow-y-auto p-2">
{/* Tasks Section */}
<div>
<h3 className="mb-2 text-xs font-semibold text-gray-600 dark:text-gray-400">
Tasks ({filteredTasks.length})
</h3>
{filteredTasks.length === 0 ? (
<div className="py-4 text-center text-xs text-gray-500">
{searchQuery ? 'No tasks match' : 'No tasks'}
</div>
) : (
<div className="space-y-2">
{filteredTasks.map(([taskId, task]) => {
const isExpanded = expandedRawTasks.has(taskId);
return (
<div
key=[redacted]
className="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-950"
>
<div
className={cn(
'flex cursor-pointer items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-900/80',
isExpanded ? 'rounded-t-lg' : 'rounded-lg',
)}
onClick={() => toggleRawTask(taskId)}
>
<div className="flex flex-1 flex-col gap-1">
<span className="font-mono text-xs font-medium">{taskId}</span>
{task?.conversationId && (
<button
onClick={(e) => {
e.stopPropagation();
onCopy(task.conversationId, `${taskId}-conversationId`);
}}
className="group/copy flex w-fit items-center gap-1 font-mono text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<span className="max-w-[300px] truncate">
Conv: {task.conversationId}
</span>
{copiedId === `${taskId}-conversationId` ? (
<Check className="h-3 w-3 shrink-0 text-green-600" />
) : (
<Copy className="h-3 w-3 shrink-0 opacity-0 group-hover/copy:opacity-100" />
)}
</button>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify(task), taskId);
}}
className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{copiedId === taskId ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</div>
</div>
<AnimatePresence>
{isExpanded && (
<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="rounded-b-lg p-2 pt-0"
onClick={(e) => e.stopPropagation()}
>
<JsonTreeView data={task} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-96" />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
)}
</div>
{/* Scheduled Executions Section */}
<div>
<h3 className="mb-2 text-xs font-semibold text-gray-600 dark:text-gray-400">
Scheduled Executions ({filteredExecutions.length})
</h3>
{filteredExecutions.length === 0 ? (
<div className="py-4 text-center text-xs text-gray-500">
{searchQuery ? 'No executions match' : 'No scheduled executions'}
</div>
) : (
<div className="space-y-2">
{filteredExecutions.map(([runId, execution]) => {
const isExpanded = expandedRawExecutions.has(runId);
return (
<div
key=[redacted]
className="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-950"
>
<div
className={cn(
'flex cursor-pointer items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-900/80',
isExpanded ? 'rounded-t-lg' : 'rounded-lg',
)}
onClick={() => toggleRawExecution(runId)}
>
<div className="flex flex-1 flex-col gap-1">
<span className="font-mono text-xs font-medium">{runId}</span>
{execution?.threadId && (
<button
onClick={(e) => {
e.stopPropagation();
onCopy(execution.threadId, `${runId}-threadId`);
}}
className="group/copy flex w-fit items-center gap-1 font-mono text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<span className="max-w-[300px] truncate">
Thread: {execution.threadId}
</span>
{copiedId === `${runId}-threadId` ? (
<Check className="h-3 w-3 shrink-0 text-green-600" />
) : (
<Copy className="h-3 w-3 shrink-0 opacity-0 group-hover/copy:opacity-100" />
)}
</button>
)}
{execution?.conversationId && (
<button
onClick={(e) => {
e.stopPropagation();
onCopy(execution.conversationId, `${runId}-conversationId`);
}}
className="group/copy flex w-fit items-center gap-1 font-mono text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<span className="max-w-[300px] truncate">
Conv: {execution.conversationId}
</span>
{copiedId === `${runId}-conversationId` ? (
<Check className="h-3 w-3 shrink-0 text-green-600" />
) : (
<Copy className="h-3 w-3 shrink-0 opacity-0 group-hover/copy:opacity-100" />
)}
</button>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify(execution), runId);
}}
className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{copiedId === runId ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</div>
</div>
<AnimatePresence>
{isExpanded && (
<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="rounded-b-lg p-2 pt-0"
onClick={(e) => e.stopPropagation()}
>
<JsonTreeView data={execution} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-96" />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
)}
</div>
</div>
{/* Footer */}
<div className="border-t border-gray-200 p-2 text-center text-xs text-gray-500 dark:border-gray-700 dark:text-gray-400">
{searchQuery
? `${filteredTasks.length} tasks, ${filteredExecutions.length} executions`
: `${taskEntries.length} tasks, ${executionEntries.length} scheduled executions`}
</div>
</div>
);
};