mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-15 19:07:23 +00:00
Integration with TaskMaster AI
This commit is contained in:
@@ -22,6 +22,8 @@ import { useDropzone } from 'react-dropzone';
|
||||
import TodoList from './TodoList';
|
||||
import ClaudeLogo from './ClaudeLogo.jsx';
|
||||
import CursorLogo from './CursorLogo.jsx';
|
||||
import NextTaskBanner from './NextTaskBanner.jsx';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
import ClaudeStatus from './ClaudeStatus';
|
||||
import { MicButton } from './MicButton.jsx';
|
||||
@@ -1162,7 +1164,8 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
|
||||
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
|
||||
//
|
||||
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
|
||||
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) {
|
||||
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter, onTaskClick, onShowAllTasks }) {
|
||||
const { tasksEnabled } = useTasksSettings();
|
||||
const [input, setInput] = useState(() => {
|
||||
if (typeof window !== 'undefined' && selectedProject) {
|
||||
return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
||||
@@ -3092,6 +3095,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
: 'Select a provider above to begin'
|
||||
}
|
||||
</p>
|
||||
|
||||
{/* Show NextTaskBanner when provider is selected and ready */}
|
||||
{provider && tasksEnabled && (
|
||||
<div className="mt-4 px-4 sm:px-0">
|
||||
<NextTaskBanner
|
||||
onStartTask={() => setInput('Start the next task')}
|
||||
onShowAllTasks={onShowAllTasks}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedSession && (
|
||||
@@ -3100,6 +3113,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
<p className="text-sm sm:text-base leading-relaxed">
|
||||
Ask questions about your code, request changes, or get help with development tasks
|
||||
</p>
|
||||
|
||||
{/* Show NextTaskBanner for existing sessions too */}
|
||||
{tasksEnabled && (
|
||||
<div className="mt-4 px-4 sm:px-0">
|
||||
<NextTaskBanner
|
||||
onStartTask={() => setInput('Start the next task')}
|
||||
onShowAllTasks={onShowAllTasks}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
88
src/components/CreateTaskModal.jsx
Normal file
88
src/components/CreateTaskModal.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { X, Sparkles } from 'lucide-react';
|
||||
|
||||
const CreateTaskModal = ({ currentProject, onClose, onTaskCreated }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-200 dark:border-gray-700">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
||||
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Create AI-Generated Task</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* AI-First Approach */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||
💡 Pro Tip: Ask Claude Code Directly!
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 mb-3">
|
||||
You can simply ask Claude Code in the chat to create tasks for you.
|
||||
The AI assistant will automatically generate detailed tasks with research-backed insights.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded border border-blue-200 dark:border-blue-700 p-3 mb-3">
|
||||
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Example:</p>
|
||||
<p className="text-sm text-gray-900 dark:text-white font-mono">
|
||||
"Please add a new task to implement user profile image uploads using Cloudinary, research the best approach."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
<strong>This runs:</strong> <code className="bg-blue-100 dark:bg-blue-900/50 px-1 rounded text-xs">
|
||||
task-master add-task --prompt="Implement user profile image uploads using Cloudinary" --research
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Learn More Link */}
|
||||
<div className="text-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
For more examples and advanced usage patterns:
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master/blob/main/docs/examples.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-medium"
|
||||
>
|
||||
View TaskMaster Documentation →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Got it, I'll ask Claude Code directly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTaskModal;
|
||||
@@ -20,6 +20,13 @@ import GitPanel from './GitPanel';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import TaskList from './TaskList';
|
||||
import TaskDetail from './TaskDetail';
|
||||
import PRDEditor from './PRDEditor';
|
||||
import Tooltip from './Tooltip';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
@@ -46,6 +53,60 @@ function MainContent({
|
||||
sendByCtrlEnter // Send by Ctrl+Enter mode for East Asian language input
|
||||
}) {
|
||||
const [editingFile, setEditingFile] = useState(null);
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||
|
||||
// PRD Editor state
|
||||
const [showPRDEditor, setShowPRDEditor] = useState(false);
|
||||
const [selectedPRD, setSelectedPRD] = useState(null);
|
||||
const [existingPRDs, setExistingPRDs] = useState([]);
|
||||
const [prdNotification, setPRDNotification] = useState(null);
|
||||
|
||||
// TaskMaster context
|
||||
const { tasks, currentProject, refreshTasks, setCurrentProject } = useTaskMaster();
|
||||
const { tasksEnabled, isTaskMasterInstalled, isTaskMasterReady } = useTasksSettings();
|
||||
|
||||
// Only show tasks tab if TaskMaster is installed and enabled
|
||||
const shouldShowTasksTab = tasksEnabled && isTaskMasterInstalled;
|
||||
|
||||
// Sync selectedProject with TaskMaster context
|
||||
useEffect(() => {
|
||||
if (selectedProject && selectedProject !== currentProject) {
|
||||
setCurrentProject(selectedProject);
|
||||
}
|
||||
}, [selectedProject, currentProject, setCurrentProject]);
|
||||
|
||||
// Switch away from tasks tab when tasks are disabled or TaskMaster is not installed
|
||||
useEffect(() => {
|
||||
if (!shouldShowTasksTab && activeTab === 'tasks') {
|
||||
setActiveTab('chat');
|
||||
}
|
||||
}, [shouldShowTasksTab, activeTab, setActiveTab]);
|
||||
|
||||
// Load existing PRDs when current project changes
|
||||
useEffect(() => {
|
||||
const loadExistingPRDs = async () => {
|
||||
if (!currentProject?.name) {
|
||||
setExistingPRDs([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setExistingPRDs(data.prdFiles || []);
|
||||
} else {
|
||||
setExistingPRDs([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load existing PRDs:', error);
|
||||
setExistingPRDs([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadExistingPRDs();
|
||||
}, [currentProject?.name]);
|
||||
|
||||
const handleFileOpen = (filePath, diffInfo = null) => {
|
||||
// Create a file object that CodeEditor expects
|
||||
@@ -61,6 +122,31 @@ function MainContent({
|
||||
const handleCloseEditor = () => {
|
||||
setEditingFile(null);
|
||||
};
|
||||
|
||||
const handleTaskClick = (task) => {
|
||||
// If task is just an ID (from dependency click), find the full task object
|
||||
if (typeof task === 'object' && task.id && !task.title) {
|
||||
const fullTask = tasks?.find(t => t.id === task.id);
|
||||
if (fullTask) {
|
||||
setSelectedTask(fullTask);
|
||||
setShowTaskDetail(true);
|
||||
}
|
||||
} else {
|
||||
setSelectedTask(task);
|
||||
setShowTaskDetail(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskDetailClose = () => {
|
||||
setShowTaskDetail(false);
|
||||
setSelectedTask(null);
|
||||
};
|
||||
|
||||
const handleTaskStatusChange = (taskId, newStatus) => {
|
||||
// This would integrate with TaskMaster API to update task status
|
||||
console.log('Update task status:', taskId, newStatus);
|
||||
refreshTasks?.();
|
||||
};
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -187,7 +273,10 @@ function MainContent({
|
||||
) : (
|
||||
<div>
|
||||
<h2 className="text-base sm:text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
|
||||
{activeTab === 'files' ? 'Project Files' :
|
||||
activeTab === 'git' ? 'Source Control' :
|
||||
(activeTab === 'tasks' && shouldShowTasksTab) ? 'TaskMaster' :
|
||||
'Project'}
|
||||
</h2>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{selectedProject.displayName}
|
||||
@@ -201,66 +290,93 @@ function MainContent({
|
||||
{/* Modern Tab Navigation - Right Side */}
|
||||
<div className="flex-shrink-0 hidden sm:block">
|
||||
<div className="relative flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
|
||||
activeTab === 'chat'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Chat</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('shell')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'shell'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Shell</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('files')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'files'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Files</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('git')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'git'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Source Control</span>
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip content="Chat" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
|
||||
activeTab === 'chat'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Chat</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Shell" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('shell')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'shell'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Shell</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Files" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('files')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'files'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Files</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Source Control" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('git')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'git'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Source Control</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{shouldShowTasksTab && (
|
||||
<Tooltip content="Tasks" position="bottom">
|
||||
<button
|
||||
onClick={() => setActiveTab('tasks')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'tasks'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
<span className="hidden md:hidden lg:inline">Tasks</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* <button
|
||||
onClick={() => setActiveTab('preview')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
@@ -302,6 +418,7 @@ function MainContent({
|
||||
showRawParameters={showRawParameters}
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
sendByCtrlEnter={sendByCtrlEnter}
|
||||
onShowAllTasks={tasksEnabled ? () => setActiveTab('tasks') : null}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@@ -318,6 +435,40 @@ function MainContent({
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'git' ? 'block' : 'hidden'}`}>
|
||||
<GitPanel selectedProject={selectedProject} isMobile={isMobile} />
|
||||
</div>
|
||||
{shouldShowTasksTab && (
|
||||
<div className={`h-full ${activeTab === 'tasks' ? 'block' : 'hidden'}`}>
|
||||
<div className="h-full flex flex-col overflow-hidden">
|
||||
<TaskList
|
||||
tasks={tasks || []}
|
||||
onTaskClick={handleTaskClick}
|
||||
showParentTasks={true}
|
||||
className="flex-1 overflow-y-auto p-4"
|
||||
currentProject={currentProject}
|
||||
onTaskCreated={refreshTasks}
|
||||
onShowPRDEditor={(prd = null) => {
|
||||
setSelectedPRD(prd);
|
||||
setShowPRDEditor(true);
|
||||
}}
|
||||
existingPRDs={existingPRDs}
|
||||
onRefreshPRDs={(showNotification = false) => {
|
||||
// Reload existing PRDs
|
||||
if (currentProject?.name) {
|
||||
api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`)
|
||||
.then(response => response.ok ? response.json() : Promise.reject())
|
||||
.then(data => {
|
||||
setExistingPRDs(data.prdFiles || []);
|
||||
if (showNotification) {
|
||||
setPRDNotification('PRD saved successfully!');
|
||||
setTimeout(() => setPRDNotification(null), 3000);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Failed to refresh PRDs:', error));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'preview' ? 'block' : 'hidden'}`}>
|
||||
{/* <LivePreviewPanel
|
||||
selectedProject={selectedProject}
|
||||
@@ -354,6 +505,63 @@ function MainContent({
|
||||
projectPath={selectedProject?.path}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Task Detail Modal */}
|
||||
{shouldShowTasksTab && showTaskDetail && selectedTask && (
|
||||
<TaskDetail
|
||||
task={selectedTask}
|
||||
isOpen={showTaskDetail}
|
||||
onClose={handleTaskDetailClose}
|
||||
onStatusChange={handleTaskStatusChange}
|
||||
onTaskClick={handleTaskClick}
|
||||
/>
|
||||
)}
|
||||
{/* PRD Editor Modal */}
|
||||
{showPRDEditor && (
|
||||
<PRDEditor
|
||||
project={currentProject}
|
||||
projectPath={currentProject?.fullPath || currentProject?.path}
|
||||
onClose={() => {
|
||||
setShowPRDEditor(false);
|
||||
setSelectedPRD(null);
|
||||
}}
|
||||
isNewFile={!selectedPRD?.isExisting}
|
||||
file={{
|
||||
name: selectedPRD?.name || 'prd.txt',
|
||||
content: selectedPRD?.content || ''
|
||||
}}
|
||||
onSave={async () => {
|
||||
setShowPRDEditor(false);
|
||||
setSelectedPRD(null);
|
||||
|
||||
// Reload existing PRDs with notification
|
||||
try {
|
||||
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setExistingPRDs(data.prdFiles || []);
|
||||
setPRDNotification('PRD saved successfully!');
|
||||
setTimeout(() => setPRDNotification(null), 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh PRDs:', error);
|
||||
}
|
||||
|
||||
refreshTasks?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* PRD Notification */}
|
||||
{prdNotification && (
|
||||
<div className="fixed bottom-4 right-4 z-50 animate-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="bg-green-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="font-medium">{prdNotification}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { MessageSquare, Folder, Terminal, GitBranch, Globe } from 'lucide-react';
|
||||
import { MessageSquare, Folder, Terminal, GitBranch, Globe, CheckSquare } from 'lucide-react';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
||||
const { tasksEnabled } = useTasksSettings();
|
||||
// Detect dark mode
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
const navItems = [
|
||||
@@ -24,7 +26,13 @@ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
||||
id: 'git',
|
||||
icon: GitBranch,
|
||||
onClick: () => setActiveTab('git')
|
||||
}
|
||||
},
|
||||
// Conditionally add tasks tab if enabled
|
||||
...(tasksEnabled ? [{
|
||||
id: 'tasks',
|
||||
icon: CheckSquare,
|
||||
onClick: () => setActiveTab('tasks')
|
||||
}] : [])
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
695
src/components/NextTaskBanner.jsx
Normal file
695
src/components/NextTaskBanner.jsx
Normal file
@@ -0,0 +1,695 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowRight, List, Clock, Flag, CheckCircle, Circle, AlertCircle, Pause, ChevronDown, ChevronUp, Plus, FileText, Settings, X, Terminal, Eye, Play, Zap, Target } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { api } from '../utils/api';
|
||||
import Shell from './Shell';
|
||||
import TaskDetail from './TaskDetail';
|
||||
|
||||
const NextTaskBanner = ({ onShowAllTasks, onStartTask, className = '' }) => {
|
||||
const { nextTask, tasks, currentProject, isLoadingTasks, projectTaskMaster, refreshTasks, refreshProjects } = useTaskMaster();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [showTaskOptions, setShowTaskOptions] = useState(false);
|
||||
const [showCreateTaskModal, setShowCreateTaskModal] = useState(false);
|
||||
const [showTemplateSelector, setShowTemplateSelector] = useState(false);
|
||||
const [showCLI, setShowCLI] = useState(false);
|
||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Handler functions
|
||||
const handleInitializeTaskMaster = async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.taskmaster.init(currentProject.name);
|
||||
if (response.ok) {
|
||||
await refreshProjects();
|
||||
setShowTaskOptions(false);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to initialize TaskMaster:', error);
|
||||
alert(`Failed to initialize TaskMaster: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing TaskMaster:', error);
|
||||
alert('Error initializing TaskMaster. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateManualTask = () => {
|
||||
setShowCreateTaskModal(true);
|
||||
setShowTaskOptions(false);
|
||||
};
|
||||
|
||||
const handleParsePRD = () => {
|
||||
setShowTemplateSelector(true);
|
||||
setShowTaskOptions(false);
|
||||
};
|
||||
|
||||
// Don't show if no project or still loading
|
||||
if (!currentProject || isLoadingTasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let bannerContent;
|
||||
|
||||
// Show setup message only if no tasks exist AND TaskMaster is not configured
|
||||
if ((!tasks || tasks.length === 0) && !projectTaskMaster?.hasTaskmaster) {
|
||||
bannerContent = (
|
||||
<div className={cn(
|
||||
'bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-3 mb-4',
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
TaskMaster AI is not configured
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setShowTaskOptions(!showTaskOptions)}
|
||||
className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Settings className="w-3 h-3" />
|
||||
Initialize TaskMaster AI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showTaskOptions && (
|
||||
<div className="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
|
||||
{!projectTaskMaster?.hasTaskmaster && (
|
||||
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/50 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
🎯 What is TaskMaster?
|
||||
</h4>
|
||||
<div className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
|
||||
<p>• <strong>AI-Powered Task Management:</strong> Break complex projects into manageable subtasks</p>
|
||||
<p>• <strong>PRD Templates:</strong> Generate tasks from Product Requirements Documents</p>
|
||||
<p>• <strong>Dependency Tracking:</strong> Understand task relationships and execution order</p>
|
||||
<p>• <strong>Progress Visualization:</strong> Kanban boards and detailed task analytics</p>
|
||||
<p>• <strong>CLI Integration:</strong> Use taskmaster commands for advanced workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{!projectTaskMaster?.hasTaskmaster ? (
|
||||
<button
|
||||
className="text-xs px-3 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 rounded transition-colors text-left flex items-center gap-2"
|
||||
onClick={() => setShowCLI(true)}
|
||||
>
|
||||
<Terminal className="w-3 h-3" />
|
||||
Initialize TaskMaster
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-2 p-2 bg-green-50 dark:bg-green-900/30 rounded text-xs text-green-800 dark:text-green-200">
|
||||
<strong>Add more tasks:</strong> Create additional tasks manually or generate them from a PRD template
|
||||
</div>
|
||||
<button
|
||||
className="text-xs px-3 py-2 bg-green-100 dark:bg-green-900 hover:bg-green-200 dark:hover:bg-green-800 text-green-800 dark:text-green-200 rounded transition-colors text-left flex items-center gap-2 disabled:opacity-50"
|
||||
onClick={handleCreateManualTask}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
Create a new task manually
|
||||
</button>
|
||||
<button
|
||||
className="text-xs px-3 py-2 bg-purple-100 dark:bg-purple-900 hover:bg-purple-200 dark:hover:bg-purple-800 text-purple-800 dark:text-purple-200 rounded transition-colors text-left flex items-center gap-2 disabled:opacity-50"
|
||||
onClick={handleParsePRD}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
{isLoading ? 'Parsing...' : 'Generate tasks from PRD template'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (nextTask) {
|
||||
// Show next task if available
|
||||
bannerContent = (
|
||||
<div className={cn(
|
||||
'bg-slate-50 dark:bg-slate-900/30 border border-slate-200 dark:border-slate-700 rounded-lg p-3 mb-4',
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-5 h-5 bg-blue-100 dark:bg-blue-900/50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Target className="w-3 h-3 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400 font-medium">Task {nextTask.id}</span>
|
||||
{nextTask.priority === 'high' && (
|
||||
<div className="w-4 h-4 rounded bg-red-100 dark:bg-red-900/50 flex items-center justify-center" title="High Priority">
|
||||
<Zap className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
)}
|
||||
{nextTask.priority === 'medium' && (
|
||||
<div className="w-4 h-4 rounded bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center" title="Medium Priority">
|
||||
<Flag className="w-2.5 h-2.5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
)}
|
||||
{nextTask.priority === 'low' && (
|
||||
<div className="w-4 h-4 rounded bg-gray-100 dark:bg-gray-800 flex items-center justify-center" title="Low Priority">
|
||||
<Circle className="w-2.5 h-2.5 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 line-clamp-1">
|
||||
{nextTask.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onStartTask?.()}
|
||||
className="text-xs px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium transition-colors shadow-sm flex items-center gap-1"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
Start Task
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowTaskDetail(true)}
|
||||
className="text-xs px-2 py-1.5 border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-md transition-colors flex items-center gap-1"
|
||||
title="View task details"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
</button>
|
||||
{onShowAllTasks && (
|
||||
<button
|
||||
onClick={onShowAllTasks}
|
||||
className="text-xs px-2 py-1.5 border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-md transition-colors flex items-center gap-1"
|
||||
title="View all tasks"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
} else if (tasks && tasks.length > 0) {
|
||||
// Show completion message only if there are tasks and all are done
|
||||
const completedTasks = tasks.filter(task => task.status === 'done').length;
|
||||
const totalTasks = tasks.length;
|
||||
|
||||
bannerContent = (
|
||||
<div className={cn(
|
||||
'bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800 rounded-lg p-3 mb-4',
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{completedTasks === totalTasks ? "All done! 🎉" : "No pending tasks"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{completedTasks}/{totalTasks}
|
||||
</span>
|
||||
<button
|
||||
onClick={onShowAllTasks}
|
||||
className="text-xs px-2 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded transition-colors"
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// TaskMaster is configured but no tasks exist - don't show anything in chat
|
||||
bannerContent = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{bannerContent}
|
||||
|
||||
{/* Create Task Modal */}
|
||||
{showCreateTaskModal && (
|
||||
<CreateTaskModal
|
||||
currentProject={currentProject}
|
||||
onClose={() => setShowCreateTaskModal(false)}
|
||||
onTaskCreated={() => {
|
||||
refreshTasks();
|
||||
setShowCreateTaskModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Template Selector Modal */}
|
||||
{showTemplateSelector && (
|
||||
<TemplateSelector
|
||||
currentProject={currentProject}
|
||||
onClose={() => setShowTemplateSelector(false)}
|
||||
onTemplateApplied={() => {
|
||||
refreshTasks();
|
||||
setShowTemplateSelector(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TaskMaster CLI Setup Modal */}
|
||||
{showCLI && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-4xl h-[600px] flex flex-col">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
||||
<Terminal className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">TaskMaster Setup</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Interactive CLI for {currentProject?.displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCLI(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Terminal Container */}
|
||||
<div className="flex-1 p-4">
|
||||
<div className="h-full bg-black rounded-lg overflow-hidden">
|
||||
<Shell
|
||||
selectedProject={currentProject}
|
||||
selectedSession={null}
|
||||
isActive={true}
|
||||
initialCommand="npx task-master init"
|
||||
isPlainShell={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
TaskMaster initialization will start automatically
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCLI(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task Detail Modal */}
|
||||
{showTaskDetail && nextTask && (
|
||||
<TaskDetail
|
||||
task={nextTask}
|
||||
isOpen={showTaskDetail}
|
||||
onClose={() => setShowTaskDetail(false)}
|
||||
onStatusChange={() => refreshTasks?.()}
|
||||
onTaskClick={null} // Disable dependency navigation in NextTaskBanner for now
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Simple Create Task Modal Component
|
||||
const CreateTaskModal = ({ currentProject, onClose, onTaskCreated }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
priority: 'medium',
|
||||
useAI: false,
|
||||
prompt: ''
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const taskData = formData.useAI
|
||||
? { prompt: formData.prompt, priority: formData.priority }
|
||||
: { title: formData.title, description: formData.description, priority: formData.priority };
|
||||
|
||||
const response = await api.taskmaster.addTask(currentProject.name, taskData);
|
||||
|
||||
if (response.ok) {
|
||||
onTaskCreated();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to create task:', error);
|
||||
alert(`Failed to create task: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
alert('Error creating task. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Create New Task</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.useAI}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, useAI: e.target.checked }))}
|
||||
/>
|
||||
Use AI to generate task details
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{formData.useAI ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Task Description (AI will generate details)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.prompt}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, prompt: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
rows="3"
|
||||
placeholder="Describe what you want to accomplish..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Task Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Enter task title..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
rows="3"
|
||||
placeholder="Describe the task..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded disabled:opacity-50"
|
||||
disabled={isSubmitting || (formData.useAI && !formData.prompt.trim()) || (!formData.useAI && (!formData.title.trim() || !formData.description.trim()))}
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create Task'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Template Selector Modal Component
|
||||
const TemplateSelector = ({ currentProject, onClose, onTemplateApplied }) => {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
||||
const [customizations, setCustomizations] = useState({});
|
||||
const [fileName, setFileName] = useState('prd.txt');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [step, setStep] = useState('select'); // 'select', 'customize', 'generate'
|
||||
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const response = await api.taskmaster.getTemplates();
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTemplates(data.templates);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTemplates();
|
||||
}, []);
|
||||
|
||||
const handleSelectTemplate = (template) => {
|
||||
setSelectedTemplate(template);
|
||||
// Find placeholders in template content
|
||||
const placeholders = template.content.match(/\[([^\]]+)\]/g) || [];
|
||||
const uniquePlaceholders = [...new Set(placeholders.map(p => p.slice(1, -1)))];
|
||||
|
||||
const initialCustomizations = {};
|
||||
uniquePlaceholders.forEach(placeholder => {
|
||||
initialCustomizations[placeholder] = '';
|
||||
});
|
||||
|
||||
setCustomizations(initialCustomizations);
|
||||
setStep('customize');
|
||||
};
|
||||
|
||||
const handleApplyTemplate = async () => {
|
||||
if (!selectedTemplate || !currentProject) return;
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
// Apply template
|
||||
const applyResponse = await api.taskmaster.applyTemplate(currentProject.name, {
|
||||
templateId: selectedTemplate.id,
|
||||
fileName,
|
||||
customizations
|
||||
});
|
||||
|
||||
if (!applyResponse.ok) {
|
||||
const error = await applyResponse.json();
|
||||
throw new Error(error.message || 'Failed to apply template');
|
||||
}
|
||||
|
||||
// Parse PRD to generate tasks
|
||||
const parseResponse = await api.taskmaster.parsePRD(currentProject.name, {
|
||||
fileName,
|
||||
numTasks: 10
|
||||
});
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
const error = await parseResponse.json();
|
||||
throw new Error(error.message || 'Failed to generate tasks');
|
||||
}
|
||||
|
||||
setStep('generate');
|
||||
setTimeout(() => {
|
||||
onTemplateApplied();
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error applying template:', error);
|
||||
alert(`Error: ${error.message}`);
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading templates...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{step === 'select' ? 'Select PRD Template' :
|
||||
step === 'customize' ? 'Customize Template' :
|
||||
'Generating Tasks'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{step === 'select' && (
|
||||
<div className="space-y-3">
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className="p-4 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
onClick={() => handleSelectTemplate(template)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">{template.name}</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{template.description}</p>
|
||||
<span className="inline-block text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded mt-2">
|
||||
{template.category}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-gray-400 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'customize' && selectedTemplate && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
File Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="prd.txt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{Object.keys(customizations).length > 0 && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Customize Template
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(customizations).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<label className="block text-xs text-gray-600 dark:text-gray-400 mb-1">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setCustomizations(prev => ({ ...prev, [key]: e.target.value }))}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder={`Enter ${key.toLowerCase()}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button
|
||||
onClick={() => setStep('select')}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleApplyTemplate}
|
||||
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded disabled:opacity-50"
|
||||
disabled={isApplying}
|
||||
>
|
||||
{isApplying ? 'Applying...' : 'Apply & Generate Tasks'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'generate' && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Template Applied Successfully!
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Your PRD has been created and tasks are being generated...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NextTaskBanner;
|
||||
871
src/components/PRDEditor.jsx
Normal file
871
src/components/PRDEditor.jsx
Normal file
@@ -0,0 +1,871 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { X, Save, Download, Maximize2, Minimize2, Eye, FileText, Sparkles, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { api, authenticatedFetch } from '../utils/api';
|
||||
|
||||
const PRDEditor = ({
|
||||
file,
|
||||
onClose,
|
||||
projectPath,
|
||||
project, // Add project object
|
||||
initialContent = '',
|
||||
isNewFile = false,
|
||||
onSave
|
||||
}) => {
|
||||
const [content, setContent] = useState(initialContent);
|
||||
const [loading, setLoading] = useState(!isNewFile);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(true);
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [previewMode, setPreviewMode] = useState(false);
|
||||
const [wordWrap, setWordWrap] = useState(true); // Default to true for markdown
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [showGenerateModal, setShowGenerateModal] = useState(false);
|
||||
const [showOverwriteConfirm, setShowOverwriteConfirm] = useState(false);
|
||||
const [existingPRDs, setExistingPRDs] = useState([]);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const PRD_TEMPLATE = `# Product Requirements Document - Example Project
|
||||
|
||||
## 1. Overview
|
||||
**Product Name:** AI-Powered Task Manager
|
||||
**Version:** 1.0
|
||||
**Date:** 2024-12-27
|
||||
**Author:** Development Team
|
||||
|
||||
This document outlines the requirements for building an AI-powered task management application that integrates with development workflows and provides intelligent task breakdown and prioritization.
|
||||
|
||||
## 2. Objectives
|
||||
- Create an intuitive task management system that works seamlessly with developer tools
|
||||
- Provide AI-powered task generation from high-level requirements
|
||||
- Enable real-time collaboration and progress tracking
|
||||
- Integrate with popular development environments (VS Code, Cursor, etc.)
|
||||
|
||||
### Success Metrics
|
||||
- User adoption rate > 80% within development teams
|
||||
- Task completion rate improvement of 25%
|
||||
- Time-to-delivery reduction of 15%
|
||||
|
||||
## 3. User Stories
|
||||
|
||||
### Core Functionality
|
||||
- As a project manager, I want to create PRDs that automatically generate detailed tasks so I can save time on project planning
|
||||
- As a developer, I want to see my next task clearly highlighted so I can maintain focus
|
||||
- As a team lead, I want to track progress across multiple projects so I can provide accurate status updates
|
||||
- As a developer, I want tasks to be broken down into implementable subtasks so I can work more efficiently
|
||||
|
||||
### AI Integration
|
||||
- As a user, I want to describe a feature in natural language and get detailed implementation tasks so I can start working immediately
|
||||
- As a project manager, I want the AI to analyze task complexity and suggest appropriate time estimates
|
||||
- As a developer, I want intelligent task prioritization based on dependencies and deadlines
|
||||
|
||||
### Collaboration
|
||||
- As a team member, I want to see real-time updates when tasks are completed so I can coordinate my work
|
||||
- As a stakeholder, I want to view project progress through intuitive dashboards
|
||||
- As a developer, I want to add implementation notes to tasks for future reference
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### Task Management
|
||||
- Create, edit, and delete tasks with rich metadata (priority, status, dependencies, estimates)
|
||||
- Hierarchical task structure with subtasks and sub-subtasks
|
||||
- Real-time status updates and progress tracking
|
||||
- Dependency management with circular dependency detection
|
||||
- Bulk operations (move, update status, assign)
|
||||
|
||||
### AI Features
|
||||
- Natural language PRD parsing to generate structured tasks
|
||||
- Intelligent task breakdown with complexity analysis
|
||||
- Automated subtask generation with implementation details
|
||||
- Smart dependency suggestion
|
||||
- Progress prediction based on historical data
|
||||
|
||||
### Integration Features
|
||||
- VS Code/Cursor extension for in-editor task management
|
||||
- Git integration for linking commits to tasks
|
||||
- API for third-party tool integration
|
||||
- Webhook support for external notifications
|
||||
- CLI tool for command-line task management
|
||||
|
||||
### User Interface
|
||||
- Responsive web application (desktop and mobile)
|
||||
- Multiple view modes (Kanban, list, calendar)
|
||||
- Dark/light theme support
|
||||
- Drag-and-drop task organization
|
||||
- Advanced filtering and search capabilities
|
||||
- Keyboard shortcuts for power users
|
||||
|
||||
## 5. Technical Requirements
|
||||
|
||||
### Frontend
|
||||
- React.js with TypeScript for type safety
|
||||
- Modern UI framework (Tailwind CSS)
|
||||
- State management (Context API or Redux)
|
||||
- Real-time updates via WebSockets
|
||||
- Progressive Web App (PWA) support
|
||||
- Accessibility compliance (WCAG 2.1 AA)
|
||||
|
||||
### Backend
|
||||
- Node.js with Express.js framework
|
||||
- RESTful API design with OpenAPI documentation
|
||||
- Real-time communication via Socket.io
|
||||
- Background job processing
|
||||
- Rate limiting and security middleware
|
||||
|
||||
### AI Integration
|
||||
- Integration with multiple AI providers (OpenAI, Anthropic, etc.)
|
||||
- Fallback model support
|
||||
- Context-aware prompt engineering
|
||||
- Token usage optimization
|
||||
- Model response caching
|
||||
|
||||
### Database
|
||||
- Primary: PostgreSQL for relational data
|
||||
- Cache: Redis for session management and real-time features
|
||||
- Full-text search capabilities
|
||||
- Database migrations and seeding
|
||||
- Backup and recovery procedures
|
||||
|
||||
### Infrastructure
|
||||
- Docker containerization
|
||||
- Cloud deployment (AWS/GCP/Azure)
|
||||
- Auto-scaling capabilities
|
||||
- Monitoring and logging (structured logging)
|
||||
- CI/CD pipeline with automated testing
|
||||
|
||||
## 6. Non-Functional Requirements
|
||||
|
||||
### Performance
|
||||
- Page load time < 2 seconds
|
||||
- API response time < 500ms for 95% of requests
|
||||
- Support for 1000+ concurrent users
|
||||
- Efficient handling of large task lists (10,000+ tasks)
|
||||
|
||||
### Security
|
||||
- JWT-based authentication with refresh tokens
|
||||
- Role-based access control (RBAC)
|
||||
- Data encryption at rest and in transit
|
||||
- Regular security audits and penetration testing
|
||||
- GDPR and privacy compliance
|
||||
|
||||
### Reliability
|
||||
- 99.9% uptime SLA
|
||||
- Graceful error handling and recovery
|
||||
- Data backup every 6 hours with point-in-time recovery
|
||||
- Disaster recovery plan with RTO < 4 hours
|
||||
|
||||
### Scalability
|
||||
- Horizontal scaling for both frontend and backend
|
||||
- Database read replicas for query optimization
|
||||
- CDN for static asset delivery
|
||||
- Microservices architecture for future expansion
|
||||
|
||||
## 7. User Experience Design
|
||||
|
||||
### Information Architecture
|
||||
- Intuitive navigation with breadcrumbs
|
||||
- Context-aware menus and actions
|
||||
- Progressive disclosure of complex features
|
||||
- Consistent design patterns throughout
|
||||
|
||||
### Interaction Design
|
||||
- Smooth animations and transitions
|
||||
- Immediate feedback for user actions
|
||||
- Undo/redo functionality for critical operations
|
||||
- Smart defaults and auto-save features
|
||||
|
||||
### Visual Design
|
||||
- Modern, clean interface with plenty of whitespace
|
||||
- Consistent color scheme and typography
|
||||
- Clear visual hierarchy with proper contrast ratios
|
||||
- Iconography that supports comprehension
|
||||
|
||||
## 8. Integration Requirements
|
||||
|
||||
### Development Tools
|
||||
- VS Code extension with task panel and quick actions
|
||||
- Cursor IDE integration with AI task suggestions
|
||||
- Terminal CLI for command-line workflow
|
||||
- Browser extension for web-based tools
|
||||
|
||||
### Third-Party Services
|
||||
- GitHub/GitLab integration for issue sync
|
||||
- Slack/Discord notifications
|
||||
- Calendar integration (Google Calendar, Outlook)
|
||||
- Time tracking tools (Toggl, Harvest)
|
||||
|
||||
### APIs and Webhooks
|
||||
- RESTful API with comprehensive documentation
|
||||
- GraphQL endpoint for complex queries
|
||||
- Webhook system for external integrations
|
||||
- SDK development for major programming languages
|
||||
|
||||
## 9. Implementation Phases
|
||||
|
||||
### Phase 1: Core MVP (8-10 weeks)
|
||||
- Basic task management (CRUD operations)
|
||||
- Simple AI task generation
|
||||
- Web interface with essential features
|
||||
- User authentication and basic permissions
|
||||
|
||||
### Phase 2: Enhanced Features (6-8 weeks)
|
||||
- Advanced AI features (complexity analysis, subtask generation)
|
||||
- Real-time collaboration
|
||||
- Mobile-responsive design
|
||||
- Integration with one development tool (VS Code)
|
||||
|
||||
### Phase 3: Enterprise Features (4-6 weeks)
|
||||
- Advanced user management and permissions
|
||||
- API and webhook system
|
||||
- Performance optimization
|
||||
- Comprehensive testing and security audit
|
||||
|
||||
### Phase 4: Ecosystem Expansion (4-6 weeks)
|
||||
- Additional tool integrations
|
||||
- Mobile app development
|
||||
- Advanced analytics and reporting
|
||||
- Third-party marketplace preparation
|
||||
|
||||
## 10. Risk Assessment
|
||||
|
||||
### Technical Risks
|
||||
- AI model reliability and cost management
|
||||
- Real-time synchronization complexity
|
||||
- Database performance with large datasets
|
||||
- Integration complexity with multiple tools
|
||||
|
||||
### Business Risks
|
||||
- User adoption in competitive market
|
||||
- AI provider dependency
|
||||
- Data privacy and security concerns
|
||||
- Feature scope creep and timeline delays
|
||||
|
||||
### Mitigation Strategies
|
||||
- Implement robust error handling and fallback systems
|
||||
- Develop comprehensive testing strategy
|
||||
- Create detailed documentation and user guides
|
||||
- Establish clear project scope and change management process
|
||||
|
||||
## 11. Success Criteria
|
||||
|
||||
### Development Milestones
|
||||
- Alpha version with core features completed
|
||||
- Beta version with selected user group feedback
|
||||
- Production-ready version with full feature set
|
||||
- Post-launch iterations based on user feedback
|
||||
|
||||
### Business Metrics
|
||||
- User engagement and retention rates
|
||||
- Task completion and productivity metrics
|
||||
- Customer satisfaction scores (NPS > 50)
|
||||
- Revenue targets and subscription growth
|
||||
|
||||
## 12. Appendices
|
||||
|
||||
### Glossary
|
||||
- **PRD**: Product Requirements Document
|
||||
- **AI**: Artificial Intelligence
|
||||
- **CRUD**: Create, Read, Update, Delete
|
||||
- **API**: Application Programming Interface
|
||||
- **CI/CD**: Continuous Integration/Continuous Deployment
|
||||
|
||||
### References
|
||||
- Industry best practices for task management
|
||||
- AI integration patterns and examples
|
||||
- Security and compliance requirements
|
||||
- Performance benchmarking data
|
||||
|
||||
---
|
||||
|
||||
**Document Control:**
|
||||
- Version: 1.0
|
||||
- Last Updated: December 27, 2024
|
||||
- Next Review: January 15, 2025
|
||||
- Approved By: Product Owner, Technical Lead`;
|
||||
|
||||
// Initialize filename and load content
|
||||
useEffect(() => {
|
||||
const initializeEditor = async () => {
|
||||
// Set initial filename
|
||||
if (file?.name) {
|
||||
setFileName(file.name.replace(/\.(txt|md)$/, '')); // Remove extension for editing
|
||||
} else if (isNewFile) {
|
||||
// Generate default filename based on current date
|
||||
const now = new Date();
|
||||
const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
setFileName(`prd-${dateStr}`);
|
||||
}
|
||||
|
||||
// Load content
|
||||
if (isNewFile) {
|
||||
setContent(PRD_TEMPLATE);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If content is directly provided (for existing PRDs loaded from API)
|
||||
if (file.content) {
|
||||
setContent(file.content);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to loading from file path (legacy support)
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await api.readFile(file.projectName, file.path);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setContent(data.content || PRD_TEMPLATE);
|
||||
} catch (error) {
|
||||
console.error('Error loading PRD file:', error);
|
||||
setContent(`# Error Loading PRD\n\nError: ${error.message}\n\nFile: ${file?.name || 'New PRD'}\nPath: ${file?.path || 'Not saved yet'}\n\n${PRD_TEMPLATE}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeEditor();
|
||||
}, [file, projectPath, isNewFile]);
|
||||
|
||||
// Fetch existing PRDs to check for conflicts
|
||||
useEffect(() => {
|
||||
const fetchExistingPRDs = async () => {
|
||||
if (!project?.name) {
|
||||
console.log('No project name available:', project);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching PRDs for project:', project.name);
|
||||
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Fetched existing PRDs:', data.prds);
|
||||
setExistingPRDs(data.prds || []);
|
||||
} else {
|
||||
console.log('Failed to fetch PRDs:', response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching existing PRDs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchExistingPRDs();
|
||||
}, [project?.name]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!content.trim()) {
|
||||
alert('Please add content before saving.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.trim()) {
|
||||
alert('Please provide a filename for the PRD.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file already exists
|
||||
const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
|
||||
const existingFile = existingPRDs.find(prd => prd.name === fullFileName);
|
||||
|
||||
console.log('Save check:', {
|
||||
fullFileName,
|
||||
existingPRDs,
|
||||
existingFile,
|
||||
isExisting: file?.isExisting,
|
||||
fileObject: file,
|
||||
shouldShowModal: existingFile && !file?.isExisting
|
||||
});
|
||||
|
||||
if (existingFile && !file?.isExisting) {
|
||||
console.log('Showing overwrite confirmation modal');
|
||||
// Show confirmation modal for overwrite
|
||||
setShowOverwriteConfirm(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await performSave();
|
||||
};
|
||||
|
||||
const performSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Ensure filename has .txt extension
|
||||
const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
|
||||
|
||||
const response = await authenticatedFetch(`/api/taskmaster/prd/${encodeURIComponent(project?.name)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
fileName: fullFileName,
|
||||
content
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Save failed: ${response.status}`);
|
||||
}
|
||||
|
||||
// Show success feedback
|
||||
setSaveSuccess(true);
|
||||
setTimeout(() => setSaveSuccess(false), 2000);
|
||||
|
||||
// Update existing PRDs list
|
||||
const response2 = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
|
||||
if (response2.ok) {
|
||||
const data = await response2.json();
|
||||
setExistingPRDs(data.prds || []);
|
||||
}
|
||||
|
||||
// Call the onSave callback if provided (for UI updates)
|
||||
if (onSave) {
|
||||
await onSave();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving PRD:', error);
|
||||
alert(`Error saving PRD: ${error.message}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([content], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const downloadFileName = fileName ? `${fileName}.txt` : 'prd.txt';
|
||||
a.download = downloadFileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleGenerateTasks = async () => {
|
||||
if (!content.trim()) {
|
||||
alert('Please add content to the PRD before generating tasks.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show AI-first modal instead of simple confirm
|
||||
setShowGenerateModal(true);
|
||||
};
|
||||
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
setIsFullscreen(!isFullscreen);
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 's') {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [content]);
|
||||
|
||||
// Simple markdown to HTML converter for preview
|
||||
const renderMarkdown = (markdown) => {
|
||||
return markdown
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
|
||||
.replace(/\*(.*)\*/gim, '<em>$1</em>')
|
||||
.replace(/^\- (.*$)/gim, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/gims, '<ul>$1</ul>')
|
||||
.replace(/\n\n/gim, '</p><p>')
|
||||
.replace(/^(?!<[h|u|l])(.*$)/gim, '<p>$1</p>')
|
||||
.replace(/<\/ul>\s*<ul>/gim, '');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] md:bg-black/50 md:flex md:items-center md:justify-center">
|
||||
<div className="w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center bg-white dark:bg-gray-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading PRD...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed inset-0 z-[200] ${
|
||||
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
|
||||
} ${isFullscreen ? 'md:p-0' : ''}`}>
|
||||
<div className={cn(
|
||||
'bg-white dark:bg-gray-900 shadow-2xl flex flex-col',
|
||||
'w-full h-full md:rounded-lg md:shadow-2xl',
|
||||
isFullscreen
|
||||
? 'md:w-full md:h-full md:rounded-none'
|
||||
: 'md:w-full md:max-w-6xl md:h-[85vh] md:max-h-[85vh]'
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 min-w-0">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="w-8 h-8 bg-purple-600 rounded flex items-center justify-center flex-shrink-0">
|
||||
<FileText className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Mobile: Stack filename and tags vertically for more space */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 min-w-0">
|
||||
{/* Filename input row - full width on mobile */}
|
||||
<div className="flex items-center gap-1 min-w-0 flex-1">
|
||||
<div className="flex items-center min-w-0 flex-1 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-md px-3 py-2 focus-within:ring-2 focus-within:ring-purple-500 focus-within:border-purple-500 dark:focus-within:ring-purple-400 dark:focus-within:border-purple-400">
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
onChange={(e) => {
|
||||
// Remove invalid filename characters
|
||||
const sanitizedValue = e.target.value.replace(/[<>:"/\\|?*]/g, '');
|
||||
setFileName(sanitizedValue);
|
||||
}}
|
||||
className="font-medium text-gray-900 dark:text-white bg-transparent border-none outline-none min-w-0 flex-1 text-base sm:text-sm placeholder-gray-400 dark:placeholder-gray-500"
|
||||
placeholder="Enter PRD filename"
|
||||
maxLength={100}
|
||||
/>
|
||||
<span className="text-sm sm:text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ml-1">.txt</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => document.querySelector('input[placeholder="Enter PRD filename"]')?.focus()}
|
||||
className="p-1 text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
|
||||
title="Click to edit filename"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tags row - moves to second line on mobile for more filename space */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className="text-xs bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-300 px-2 py-1 rounded whitespace-nowrap">
|
||||
📋 PRD
|
||||
</span>
|
||||
{isNewFile && (
|
||||
<span className="text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-300 px-2 py-1 rounded whitespace-nowrap">
|
||||
✨ New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description - smaller on mobile */}
|
||||
<p className="text-xs sm:text-sm text-gray-500 dark:text-gray-400 truncate mt-1">
|
||||
Product Requirements Document
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setPreviewMode(!previewMode)}
|
||||
className={cn(
|
||||
'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
|
||||
previewMode
|
||||
? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
)}
|
||||
title={previewMode ? 'Switch to edit mode' : 'Preview markdown'}
|
||||
>
|
||||
<Eye className="w-5 h-5 md:w-4 md:h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setWordWrap(!wordWrap)}
|
||||
className={cn(
|
||||
'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
|
||||
wordWrap
|
||||
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
)}
|
||||
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
||||
>
|
||||
<span className="text-sm md:text-xs font-mono font-bold">↵</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<span className="text-lg md:text-base">{isDarkMode ? '☀️' : '🌙'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Download PRD"
|
||||
>
|
||||
<Download className="w-5 h-5 md:w-4 md:h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleGenerateTasks}
|
||||
disabled={!content.trim()}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors text-sm font-medium',
|
||||
'bg-purple-600 hover:bg-purple-700 text-white',
|
||||
'min-h-[44px] md:min-h-0'
|
||||
)}
|
||||
title="Generate tasks from PRD content"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="hidden md:inline">Generate Tasks</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
'px-3 py-2 text-white rounded-md disabled:opacity-50 flex items-center gap-2 transition-colors',
|
||||
'min-h-[44px] md:min-h-0',
|
||||
saveSuccess
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-purple-600 hover:bg-purple-700'
|
||||
)}
|
||||
>
|
||||
{saveSuccess ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Saved!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-5 h-5 md:w-4 md:h-4" />
|
||||
<span className="hidden sm:inline">{saving ? 'Saving...' : 'Save PRD'}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-6 h-6 md:w-4 md:h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor/Preview Content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{previewMode ? (
|
||||
<div className="h-full overflow-y-auto p-6 prose prose-gray dark:prose-invert max-w-none">
|
||||
<div
|
||||
className="markdown-preview"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
extensions={[
|
||||
markdown(),
|
||||
...(wordWrap ? [EditorView.lineWrapping] : [])
|
||||
]}
|
||||
theme={isDarkMode ? oneDark : undefined}
|
||||
height="100%"
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
height: '100%',
|
||||
}}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Lines: {content.split('\n').length}</span>
|
||||
<span>Characters: {content.length}</span>
|
||||
<span>Words: {content.split(/\s+/).filter(word => word.length > 0).length}</span>
|
||||
<span>Format: Markdown</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Press Ctrl+S to save • Esc to close
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Tasks Modal */}
|
||||
{showGenerateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-200 dark:border-gray-700">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center">
|
||||
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Generate Tasks from PRD</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowGenerateModal(false)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* AI-First Approach */}
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4 border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2">
|
||||
💡 Pro Tip: Ask Claude Code Directly!
|
||||
</h4>
|
||||
<p className="text-sm text-purple-800 dark:text-purple-200 mb-3">
|
||||
You can simply ask Claude Code in the chat to parse your PRD and generate tasks.
|
||||
The AI assistant will automatically save your PRD and create detailed tasks with implementation details.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded border border-purple-200 dark:border-purple-700 p-3 mb-3">
|
||||
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">💬 Example:</p>
|
||||
<p className="text-xs text-gray-900 dark:text-white font-mono">
|
||||
"I've just initialized a new project with Claude Task Master. I have a PRD at .taskmaster/docs/{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}. Can you help me parse it and set up the initial tasks?"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-purple-700 dark:text-purple-300">
|
||||
<strong>This will:</strong> Save your PRD, analyze its content, and generate structured tasks with subtasks, dependencies, and implementation details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Learn More Link */}
|
||||
<div className="text-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
For more examples and advanced usage patterns:
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master/blob/main/docs/examples.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 underline font-medium"
|
||||
>
|
||||
View TaskMaster Documentation →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={() => setShowGenerateModal(false)}
|
||||
className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Got it, I'll ask Claude Code directly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overwrite Confirmation Modal */}
|
||||
{showOverwriteConfirm && (
|
||||
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowOverwriteConfirm(false)} />
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="p-2 rounded-full mr-3 bg-yellow-100 dark:bg-yellow-900">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
File Already Exists
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
A PRD file named "{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}" already exists.
|
||||
Do you want to overwrite it with the current content?
|
||||
</p>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowOverwriteConfirm(false)}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setShowOverwriteConfirm(false);
|
||||
await performSave();
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-white bg-yellow-600 hover:bg-yellow-700 rounded-md flex items-center space-x-2 transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
<span>Overwrite</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PRDEditor;
|
||||
@@ -29,7 +29,7 @@ if (typeof document !== 'undefined') {
|
||||
// Global store for shell sessions to persist across tab switches
|
||||
const shellSessions = new Map();
|
||||
|
||||
function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
function Shell({ selectedProject, selectedSession, isActive, initialCommand, isPlainShell = false, onProcessComplete }) {
|
||||
const terminalRef = useRef(null);
|
||||
const terminal = useRef(null);
|
||||
const fitAddon = useRef(null);
|
||||
@@ -434,13 +434,17 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
const initPayload = {
|
||||
type: 'init',
|
||||
projectPath: selectedProject.fullPath || selectedProject.path,
|
||||
sessionId: selectedSession?.id,
|
||||
hasSession: !!selectedSession,
|
||||
provider: selectedSession?.__provider || 'claude',
|
||||
sessionId: isPlainShell ? null : selectedSession?.id,
|
||||
hasSession: isPlainShell ? false : !!selectedSession,
|
||||
provider: isPlainShell ? 'plain-shell' : (selectedSession?.__provider || 'claude'),
|
||||
cols: terminal.current.cols,
|
||||
rows: terminal.current.rows
|
||||
rows: terminal.current.rows,
|
||||
initialCommand: initialCommand,
|
||||
isPlainShell: isPlainShell
|
||||
};
|
||||
|
||||
console.log('Shell init payload:', initPayload);
|
||||
|
||||
ws.current.send(JSON.stringify(initPayload));
|
||||
|
||||
// Also send resize message immediately after init
|
||||
@@ -473,6 +477,18 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
urls.push(match[1]);
|
||||
}
|
||||
|
||||
if (isPlainShell && onProcessComplete) {
|
||||
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI codes
|
||||
if (cleanOutput.includes('Process exited with code 0')) {
|
||||
onProcessComplete(0); // Success
|
||||
} else if (cleanOutput.match(/Process exited with code (\d+)/)) {
|
||||
const exitCode = parseInt(cleanOutput.match(/Process exited with code (\d+)/)[1]);
|
||||
if (exitCode !== 0) {
|
||||
onProcessComplete(exitCode); // Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If URLs found, log them for potential opening
|
||||
|
||||
terminal.current.write(output);
|
||||
@@ -606,14 +622,16 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
<span>Continue in Shell</span>
|
||||
</button>
|
||||
<p className="text-gray-400 text-sm mt-3 px-2">
|
||||
{selectedSession ?
|
||||
(() => {
|
||||
const displaySessionName = selectedSession.__provider === 'cursor'
|
||||
? (selectedSession.name || 'Untitled Session')
|
||||
: (selectedSession.summary || 'New Session');
|
||||
return `Resume session: ${displaySessionName.slice(0, 50)}...`;
|
||||
})() :
|
||||
'Start a new Claude session'
|
||||
{isPlainShell ?
|
||||
`Run ${initialCommand || 'command'} in ${selectedProject.displayName}` :
|
||||
selectedSession ?
|
||||
(() => {
|
||||
const displaySessionName = selectedSession.__provider === 'cursor'
|
||||
? (selectedSession.name || 'Untitled Session')
|
||||
: (selectedSession.summary || 'New Session');
|
||||
return `Resume session: ${displaySessionName.slice(0, 50)}...`;
|
||||
})() :
|
||||
'Start a new Claude session'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
@@ -629,7 +647,10 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
<span className="text-base font-medium">Connecting to shell...</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mt-3 px-2">
|
||||
Starting Claude CLI in {selectedProject.displayName}
|
||||
{isPlainShell ?
|
||||
`Running ${initialCommand || 'command'} in ${selectedProject.displayName}` :
|
||||
`Starting Claude CLI in ${selectedProject.displayName}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,10 @@ import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRig
|
||||
import { cn } from '../lib/utils';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo.jsx';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
import { api } from '../utils/api';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
// Move formatTimeAgo outside component to avoid recreation on every render
|
||||
const formatTimeAgo = (dateString, currentTime) => {
|
||||
@@ -70,6 +73,10 @@ function Sidebar({
|
||||
const [generatingSummary, setGeneratingSummary] = useState({});
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
// TaskMaster context
|
||||
const { setCurrentProject, mcpServerStatus } = useTaskMaster();
|
||||
const { tasksEnabled } = useTasksSettings();
|
||||
|
||||
|
||||
// Starred projects state - persisted in localStorage
|
||||
const [starredProjects, setStarredProjects] = useState(() => {
|
||||
@@ -417,6 +424,15 @@ function Sidebar({
|
||||
return displayName.includes(searchLower) || projectName.includes(searchLower);
|
||||
});
|
||||
|
||||
// Enhanced project selection that updates both the main UI and TaskMaster context
|
||||
const handleProjectSelect = (project) => {
|
||||
// Call the original project select handler
|
||||
onProjectSelect(project);
|
||||
|
||||
// Update TaskMaster context with the selected project
|
||||
setCurrentProject(project);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-card md:select-none">
|
||||
{/* Header */}
|
||||
@@ -500,6 +516,7 @@ function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* New Project Form */}
|
||||
{showNewProject && (
|
||||
<div className="md:p-3 md:border-b md:border-border md:bg-muted/30">
|
||||
@@ -717,9 +734,25 @@ function Sidebar({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-sm font-medium text-foreground truncate">
|
||||
{project.displayName}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between min-w-0 flex-1">
|
||||
<h3 className="text-sm font-medium text-foreground truncate">
|
||||
{project.displayName}
|
||||
</h3>
|
||||
{tasksEnabled && (
|
||||
<TaskIndicator
|
||||
status={(() => {
|
||||
const projectConfigured = project.taskmaster?.hasTaskmaster;
|
||||
const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
|
||||
if (projectConfigured && mcpConfigured) return 'fully-configured';
|
||||
if (projectConfigured) return 'taskmaster-only';
|
||||
if (mcpConfigured) return 'mcp-only';
|
||||
return 'not-configured';
|
||||
})()}
|
||||
size="xs"
|
||||
className="flex-shrink-0 ml-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(() => {
|
||||
const sessionCount = getAllSessions(project).length;
|
||||
@@ -825,13 +858,13 @@ function Sidebar({
|
||||
onClick={() => {
|
||||
// Desktop behavior: select project and toggle
|
||||
if (selectedProject?.name !== project.name) {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
}
|
||||
toggleProject(project.name);
|
||||
}}
|
||||
onTouchEnd={handleTouchClick(() => {
|
||||
if (selectedProject?.name !== project.name) {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
}
|
||||
toggleProject(project.name);
|
||||
})}
|
||||
@@ -1013,11 +1046,11 @@ function Sidebar({
|
||||
isActive ? "border-green-500/30 bg-green-50/5 dark:bg-green-900/5" : "border-border/30"
|
||||
)}
|
||||
onClick={() => {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
onSessionSelect(session);
|
||||
}}
|
||||
onTouchEnd={handleTouchClick(() => {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
onSessionSelect(session);
|
||||
})}
|
||||
>
|
||||
@@ -1239,7 +1272,7 @@ function Sidebar({
|
||||
<button
|
||||
className="w-full h-8 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md flex items-center justify-center gap-2 font-medium text-xs active:scale-[0.98] transition-all duration-150"
|
||||
onClick={() => {
|
||||
onProjectSelect(project);
|
||||
handleProjectSelect(project);
|
||||
onNewSession(project);
|
||||
}}
|
||||
>
|
||||
|
||||
210
src/components/TaskCard.jsx
Normal file
210
src/components/TaskCard.jsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React from 'react';
|
||||
import { Clock, CheckCircle, Circle, AlertCircle, Pause, X, ArrowRight, ChevronUp, Minus, Flag } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const TaskCard = ({
|
||||
task,
|
||||
onClick,
|
||||
showParent = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const getStatusConfig = (status) => {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
bgColor: 'bg-green-50 dark:bg-green-950',
|
||||
borderColor: 'border-green-200 dark:border-green-800',
|
||||
iconColor: 'text-green-600 dark:text-green-400',
|
||||
textColor: 'text-green-900 dark:text-green-100',
|
||||
statusText: 'Done'
|
||||
};
|
||||
|
||||
case 'in-progress':
|
||||
return {
|
||||
icon: Clock,
|
||||
bgColor: 'bg-blue-50 dark:bg-blue-950',
|
||||
borderColor: 'border-blue-200 dark:border-blue-800',
|
||||
iconColor: 'text-blue-600 dark:text-blue-400',
|
||||
textColor: 'text-blue-900 dark:text-blue-100',
|
||||
statusText: 'In Progress'
|
||||
};
|
||||
|
||||
case 'review':
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
bgColor: 'bg-amber-50 dark:bg-amber-950',
|
||||
borderColor: 'border-amber-200 dark:border-amber-800',
|
||||
iconColor: 'text-amber-600 dark:text-amber-400',
|
||||
textColor: 'text-amber-900 dark:text-amber-100',
|
||||
statusText: 'Review'
|
||||
};
|
||||
|
||||
case 'deferred':
|
||||
return {
|
||||
icon: Pause,
|
||||
bgColor: 'bg-gray-50 dark:bg-gray-800',
|
||||
borderColor: 'border-gray-200 dark:border-gray-700',
|
||||
iconColor: 'text-gray-500 dark:text-gray-400',
|
||||
textColor: 'text-gray-700 dark:text-gray-300',
|
||||
statusText: 'Deferred'
|
||||
};
|
||||
|
||||
case 'cancelled':
|
||||
return {
|
||||
icon: X,
|
||||
bgColor: 'bg-red-50 dark:bg-red-950',
|
||||
borderColor: 'border-red-200 dark:border-red-800',
|
||||
iconColor: 'text-red-600 dark:text-red-400',
|
||||
textColor: 'text-red-900 dark:text-red-100',
|
||||
statusText: 'Cancelled'
|
||||
};
|
||||
|
||||
case 'pending':
|
||||
default:
|
||||
return {
|
||||
icon: Circle,
|
||||
bgColor: 'bg-slate-50 dark:bg-slate-800',
|
||||
borderColor: 'border-slate-200 dark:border-slate-700',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400',
|
||||
textColor: 'text-slate-900 dark:text-slate-100',
|
||||
statusText: 'Pending'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig(task.status);
|
||||
const Icon = config.icon;
|
||||
|
||||
const getPriorityIcon = (priority) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return (
|
||||
<Tooltip content="High Priority">
|
||||
<div className="w-4 h-4 bg-red-100 dark:bg-red-900/30 rounded flex items-center justify-center">
|
||||
<ChevronUp className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'medium':
|
||||
return (
|
||||
<Tooltip content="Medium Priority">
|
||||
<div className="w-4 h-4 bg-amber-100 dark:bg-amber-900/30 rounded flex items-center justify-center">
|
||||
<Minus className="w-2.5 h-2.5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'low':
|
||||
return (
|
||||
<Tooltip content="Low Priority">
|
||||
<div className="w-4 h-4 bg-blue-100 dark:bg-blue-900/30 rounded flex items-center justify-center">
|
||||
<Circle className="w-1.5 h-1.5 text-blue-600 dark:text-blue-400 fill-current" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip content="No Priority Set">
|
||||
<div className="w-4 h-4 bg-gray-100 dark:bg-gray-800 rounded flex items-center justify-center">
|
||||
<Circle className="w-1.5 h-1.5 text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700',
|
||||
'hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-200 cursor-pointer',
|
||||
'p-3 space-y-3',
|
||||
onClick && 'hover:-translate-y-0.5',
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Header with Task ID, Title, and Priority */}
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
{/* Task ID and Title */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Tooltip content={`Task ID: ${task.id}`}>
|
||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">
|
||||
{task.id}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<h3 className="font-medium text-sm text-gray-900 dark:text-white line-clamp-2 leading-tight">
|
||||
{task.title}
|
||||
</h3>
|
||||
{showParent && task.parentId && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
Task {task.parentId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority Icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{getPriorityIcon(task.priority)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with Dependencies and Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Dependencies */}
|
||||
<div className="flex items-center">
|
||||
{task.dependencies && Array.isArray(task.dependencies) && task.dependencies.length > 0 && (
|
||||
<Tooltip content={`Depends on: ${task.dependencies.map(dep => `Task ${dep}`).join(', ')}`}>
|
||||
<div className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
<span>Depends on: {task.dependencies.join(', ')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<Tooltip content={`Status: ${config.statusText}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className={cn('w-2 h-2 rounded-full', config.iconColor.replace('text-', 'bg-'))} />
|
||||
<span className={cn('text-xs font-medium', config.textColor)}>
|
||||
{config.statusText}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Subtask Progress (if applicable) */}
|
||||
{task.subtasks && task.subtasks.length > 0 && (
|
||||
<div className="ml-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Progress:</span>
|
||||
<Tooltip content={`${task.subtasks.filter(st => st.status === 'done').length} of ${task.subtasks.length} subtasks completed`}>
|
||||
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all duration-300',
|
||||
task.status === 'done' ? 'bg-green-500' : 'bg-blue-500'
|
||||
)}
|
||||
style={{
|
||||
width: `${Math.round((task.subtasks.filter(st => st.status === 'done').length / task.subtasks.length) * 100)}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip content={`${task.subtasks.filter(st => st.status === 'done').length} completed, ${task.subtasks.filter(st => st.status === 'pending').length} pending, ${task.subtasks.filter(st => st.status === 'in-progress').length} in progress`}>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{task.subtasks.filter(st => st.status === 'done').length}/{task.subtasks.length}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCard;
|
||||
406
src/components/TaskDetail.jsx
Normal file
406
src/components/TaskDetail.jsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Flag, User, ArrowRight, CheckCircle, Circle, AlertCircle, Pause, Edit, Save, Copy, ChevronDown, ChevronRight, Clock } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
import { api } from '../utils/api';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
|
||||
const TaskDetail = ({
|
||||
task,
|
||||
onClose,
|
||||
onEdit,
|
||||
onStatusChange,
|
||||
onTaskClick,
|
||||
isOpen = true,
|
||||
className = ''
|
||||
}) => {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editedTask, setEditedTask] = useState(task || {});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [showTestStrategy, setShowTestStrategy] = useState(false);
|
||||
const { currentProject, refreshTasks } = useTaskMaster();
|
||||
|
||||
if (!isOpen || !task) return null;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// Only include changed fields
|
||||
const updates = {};
|
||||
if (editedTask.title !== task.title) updates.title = editedTask.title;
|
||||
if (editedTask.description !== task.description) updates.description = editedTask.description;
|
||||
if (editedTask.details !== task.details) updates.details = editedTask.details;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const response = await api.taskmaster.updateTask(currentProject.name, task.id, updates);
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh tasks to get updated data
|
||||
refreshTasks?.();
|
||||
onEdit?.(editedTask);
|
||||
setEditMode(false);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to update task:', error);
|
||||
alert(`Failed to update task: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
setEditMode(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
alert('Error updating task. Please try again.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
try {
|
||||
const response = await api.taskmaster.updateTask(currentProject.name, task.id, { status: newStatus });
|
||||
|
||||
if (response.ok) {
|
||||
refreshTasks?.();
|
||||
onStatusChange?.(task.id, newStatus);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to update task status:', error);
|
||||
alert(`Failed to update task status: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task status:', error);
|
||||
alert('Error updating task status. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const copyTaskId = () => {
|
||||
navigator.clipboard.writeText(task.id.toString());
|
||||
};
|
||||
|
||||
const getStatusConfig = (status) => {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return { icon: CheckCircle, color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-950' };
|
||||
case 'in-progress':
|
||||
return { icon: Clock, color: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-950' };
|
||||
case 'review':
|
||||
return { icon: AlertCircle, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-950' };
|
||||
case 'deferred':
|
||||
return { icon: Pause, color: 'text-gray-500 dark:text-gray-400', bg: 'bg-gray-50 dark:bg-gray-800' };
|
||||
case 'cancelled':
|
||||
return { icon: X, color: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-950' };
|
||||
default:
|
||||
return { icon: Circle, color: 'text-slate-500 dark:text-slate-400', bg: 'bg-slate-50 dark:bg-slate-800' };
|
||||
}
|
||||
};
|
||||
|
||||
const statusConfig = getStatusConfig(task.status);
|
||||
const StatusIcon = statusConfig.icon;
|
||||
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950';
|
||||
case 'medium': return 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-950';
|
||||
case 'low': return 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950';
|
||||
default: return 'text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'in-progress', label: 'In Progress' },
|
||||
{ value: 'review', label: 'Review' },
|
||||
{ value: 'done', label: 'Done' },
|
||||
{ value: 'deferred', label: 'Deferred' },
|
||||
{ value: 'cancelled', label: 'Cancelled' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-black/50">
|
||||
<div className={cn(
|
||||
'bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 md:rounded-lg shadow-xl',
|
||||
'w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col',
|
||||
className
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<StatusIcon className={cn('w-6 h-6', statusConfig.color)} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<button
|
||||
onClick={copyTaskId}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
title="Click to copy task ID"
|
||||
>
|
||||
<span>Task {task.id}</span>
|
||||
<Copy className="w-3 h-3" />
|
||||
</button>
|
||||
{task.parentId && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Subtask of Task {task.parentId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{editMode ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editedTask.title || ''}
|
||||
onChange={(e) => setEditedTask({ ...editedTask, title: e.target.value })}
|
||||
className="w-full text-lg font-semibold bg-transparent border-b-2 border-blue-500 focus:outline-none text-gray-900 dark:text-white"
|
||||
placeholder="Task title"
|
||||
/>
|
||||
) : (
|
||||
<h1 className="text-lg md:text-xl font-semibold text-gray-900 dark:text-white line-clamp-2">
|
||||
{task.title}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{editMode ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="p-2 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title={isSaving ? "Saving..." : "Save changes"}
|
||||
>
|
||||
<Save className={cn("w-5 h-5", isSaving && "animate-spin")} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditMode(false);
|
||||
setEditedTask(task);
|
||||
}}
|
||||
disabled={isSaving}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Cancel editing"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setEditMode(true)}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
title="Edit task"
|
||||
>
|
||||
<Edit className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 min-h-0">
|
||||
{/* Status and Metadata Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
|
||||
<div className={cn(
|
||||
'w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600',
|
||||
statusConfig.bg,
|
||||
statusConfig.color
|
||||
)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon className="w-4 h-4" />
|
||||
<span className="font-medium capitalize">
|
||||
{statusOptions.find(option => option.value === task.status)?.label || task.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Priority</label>
|
||||
<div className={cn(
|
||||
'px-3 py-2 rounded-md text-sm font-medium capitalize',
|
||||
getPriorityColor(task.priority)
|
||||
)}>
|
||||
<Flag className="w-4 h-4 inline mr-2" />
|
||||
{task.priority || 'Not set'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dependencies */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Dependencies</label>
|
||||
{task.dependencies && task.dependencies.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{task.dependencies.map(depId => (
|
||||
<button
|
||||
key={depId}
|
||||
onClick={() => onTaskClick && onTaskClick({ id: depId })}
|
||||
className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded text-sm hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer disabled:cursor-default disabled:opacity-50"
|
||||
disabled={!onTaskClick}
|
||||
title={onTaskClick ? `Click to view Task ${depId}` : `Task ${depId}`}
|
||||
>
|
||||
<ArrowRight className="w-3 h-3 inline mr-1" />
|
||||
{depId}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400 text-sm">No dependencies</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={editedTask.description || ''}
|
||||
onChange={(e) => setEditedTask({ ...editedTask, description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Task description"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{task.description || 'No description provided'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Implementation Details */}
|
||||
{task.details && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Implementation Details
|
||||
</span>
|
||||
{showDetails ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
{showDetails && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={editedTask.details || ''}
|
||||
onChange={(e) => setEditedTask({ ...editedTask, details: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="Implementation details"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-md p-4">
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{task.details}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test Strategy */}
|
||||
{task.testStrategy && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<button
|
||||
onClick={() => setShowTestStrategy(!showTestStrategy)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Test Strategy
|
||||
</span>
|
||||
{showTestStrategy ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
{showTestStrategy && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-950 rounded-md p-4">
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{task.testStrategy}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subtasks */}
|
||||
{task.subtasks && task.subtasks.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Subtasks ({task.subtasks.length})
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{task.subtasks.map(subtask => {
|
||||
const subtaskConfig = getStatusConfig(subtask.status);
|
||||
const SubtaskIcon = subtaskConfig.icon;
|
||||
return (
|
||||
<div key={subtask.id} className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
|
||||
<SubtaskIcon className={cn('w-4 h-4', subtaskConfig.color)} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{subtask.title}
|
||||
</h4>
|
||||
{subtask.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 truncate">
|
||||
{subtask.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{subtask.id}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Task ID: {task.id}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetail;
|
||||
108
src/components/TaskIndicator.jsx
Normal file
108
src/components/TaskIndicator.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { CheckCircle, Settings, X, AlertCircle } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
/**
|
||||
* TaskIndicator Component
|
||||
*
|
||||
* Displays TaskMaster status for projects in the sidebar with appropriate
|
||||
* icons and colors based on the project's TaskMaster configuration state.
|
||||
*/
|
||||
const TaskIndicator = ({
|
||||
status = 'not-configured',
|
||||
size = 'sm',
|
||||
className = '',
|
||||
showLabel = false
|
||||
}) => {
|
||||
const getIndicatorConfig = () => {
|
||||
switch (status) {
|
||||
case 'fully-configured':
|
||||
return {
|
||||
icon: CheckCircle,
|
||||
color: 'text-green-500 dark:text-green-400',
|
||||
bgColor: 'bg-green-50 dark:bg-green-950',
|
||||
label: 'TaskMaster Ready',
|
||||
title: 'TaskMaster fully configured with MCP server'
|
||||
};
|
||||
|
||||
case 'taskmaster-only':
|
||||
return {
|
||||
icon: Settings,
|
||||
color: 'text-blue-500 dark:text-blue-400',
|
||||
bgColor: 'bg-blue-50 dark:bg-blue-950',
|
||||
label: 'TaskMaster Init',
|
||||
title: 'TaskMaster initialized, MCP server needs setup'
|
||||
};
|
||||
|
||||
case 'mcp-only':
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
color: 'text-amber-500 dark:text-amber-400',
|
||||
bgColor: 'bg-amber-50 dark:bg-amber-950',
|
||||
label: 'MCP Ready',
|
||||
title: 'MCP server configured, TaskMaster needs initialization'
|
||||
};
|
||||
|
||||
case 'not-configured':
|
||||
case 'error':
|
||||
default:
|
||||
return {
|
||||
icon: X,
|
||||
color: 'text-gray-400 dark:text-gray-500',
|
||||
bgColor: 'bg-gray-50 dark:bg-gray-900',
|
||||
label: 'No TaskMaster',
|
||||
title: 'TaskMaster not configured'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getIndicatorConfig();
|
||||
const Icon = config.icon;
|
||||
|
||||
const sizeClasses = {
|
||||
xs: 'w-3 h-3',
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6'
|
||||
};
|
||||
|
||||
const paddingClasses = {
|
||||
xs: 'p-0.5',
|
||||
sm: 'p-1',
|
||||
md: 'p-1.5',
|
||||
lg: 'p-2'
|
||||
};
|
||||
|
||||
if (showLabel) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 text-xs rounded-md px-2 py-1 transition-colors',
|
||||
config.bgColor,
|
||||
config.color,
|
||||
className
|
||||
)}
|
||||
title={config.title}
|
||||
>
|
||||
<Icon className={sizeClasses[size]} />
|
||||
<span className="font-medium">{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-full transition-colors',
|
||||
config.bgColor,
|
||||
paddingClasses[size],
|
||||
className
|
||||
)}
|
||||
title={config.title}
|
||||
>
|
||||
<Icon className={cn(sizeClasses[size], config.color)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskIndicator;
|
||||
1054
src/components/TaskList.jsx
Normal file
1054
src/components/TaskList.jsx
Normal file
File diff suppressed because it is too large
Load Diff
603
src/components/TaskMasterSetupWizard.jsx
Normal file
603
src/components/TaskMasterSetupWizard.jsx
Normal file
@@ -0,0 +1,603 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, ChevronRight, ChevronLeft, CheckCircle, AlertCircle, Settings, Server, FileText, Sparkles, ExternalLink, Copy } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
const TaskMasterSetupWizard = ({
|
||||
isOpen = true,
|
||||
onClose,
|
||||
onComplete,
|
||||
currentProject,
|
||||
className = ''
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [setupData, setSetupData] = useState({
|
||||
projectRoot: '',
|
||||
initGit: true,
|
||||
storeTasksInGit: true,
|
||||
addAliases: true,
|
||||
skipInstall: false,
|
||||
rules: ['claude'],
|
||||
mcpConfigured: false,
|
||||
prdContent: ''
|
||||
});
|
||||
|
||||
const totalSteps = 4;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProject) {
|
||||
setSetupData(prev => ({
|
||||
...prev,
|
||||
projectRoot: currentProject.path || ''
|
||||
}));
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Project Configuration',
|
||||
description: 'Configure basic TaskMaster settings for your project'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'MCP Server Setup',
|
||||
description: 'Ensure TaskMaster MCP server is properly configured'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'PRD Creation',
|
||||
description: 'Create or import a Product Requirements Document'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Complete Setup',
|
||||
description: 'Initialize TaskMaster and generate initial tasks'
|
||||
}
|
||||
];
|
||||
|
||||
const handleNext = async () => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (currentStep === 1) {
|
||||
// Validate project configuration
|
||||
if (!setupData.projectRoot) {
|
||||
setError('Project root path is required');
|
||||
return;
|
||||
}
|
||||
setCurrentStep(2);
|
||||
} else if (currentStep === 2) {
|
||||
// Check MCP server status
|
||||
setLoading(true);
|
||||
try {
|
||||
const mcpStatus = await api.get('/mcp-utils/taskmaster-server');
|
||||
setSetupData(prev => ({
|
||||
...prev,
|
||||
mcpConfigured: mcpStatus.hasMCPServer && mcpStatus.isConfigured
|
||||
}));
|
||||
setCurrentStep(3);
|
||||
} catch (err) {
|
||||
setError('Failed to check MCP server status. You can continue but some features may not work.');
|
||||
setCurrentStep(3);
|
||||
}
|
||||
} else if (currentStep === 3) {
|
||||
// Validate PRD step
|
||||
if (!setupData.prdContent.trim()) {
|
||||
setError('Please create or import a PRD to continue');
|
||||
return;
|
||||
}
|
||||
setCurrentStep(4);
|
||||
} else if (currentStep === 4) {
|
||||
// Complete setup
|
||||
await completeSetup();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message || 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const completeSetup = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Initialize TaskMaster project
|
||||
const initResponse = await api.post('/taskmaster/initialize', {
|
||||
projectRoot: setupData.projectRoot,
|
||||
initGit: setupData.initGit,
|
||||
storeTasksInGit: setupData.storeTasksInGit,
|
||||
addAliases: setupData.addAliases,
|
||||
skipInstall: setupData.skipInstall,
|
||||
rules: setupData.rules,
|
||||
yes: true
|
||||
});
|
||||
|
||||
if (!initResponse.ok) {
|
||||
throw new Error('Failed to initialize TaskMaster project');
|
||||
}
|
||||
|
||||
// Save PRD content if provided
|
||||
if (setupData.prdContent.trim()) {
|
||||
const prdResponse = await api.post('/taskmaster/save-prd', {
|
||||
projectRoot: setupData.projectRoot,
|
||||
content: setupData.prdContent
|
||||
});
|
||||
|
||||
if (!prdResponse.ok) {
|
||||
console.warn('Failed to save PRD content');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse PRD to generate initial tasks
|
||||
if (setupData.prdContent.trim()) {
|
||||
const parseResponse = await api.post('/taskmaster/parse-prd', {
|
||||
projectRoot: setupData.projectRoot,
|
||||
input: '.taskmaster/docs/prd.txt',
|
||||
numTasks: '10',
|
||||
research: false,
|
||||
force: false
|
||||
});
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
console.warn('Failed to parse PRD and generate tasks');
|
||||
}
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
onClose?.();
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to complete TaskMaster setup');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyMCPConfig = () => {
|
||||
const mcpConfig = `{
|
||||
"mcpServers": {
|
||||
"": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_anthropic_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_perplexity_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
navigator.clipboard.writeText(mcpConfig);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<Settings className="w-12 h-12 text-blue-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Project Configuration
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Configure TaskMaster settings for your project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Project Root Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={setupData.projectRoot}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, projectRoot: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="/path/to/your/project"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">Options</h4>
|
||||
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.initGit}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, initGit: e.target.checked }))}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Initialize Git repository</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.storeTasksInGit}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, storeTasksInGit: e.target.checked }))}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Store tasks in Git</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.addAliases}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, addAliases: e.target.checked }))}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Add shell aliases (tm, taskmaster)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Rule Profiles
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{['claude', 'cursor', 'vscode', 'roo', 'cline', 'windsurf'].map(rule => (
|
||||
<label key={rule} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={setupData.rules.includes(rule)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSetupData(prev => ({ ...prev, rules: [...prev.rules, rule] }));
|
||||
} else {
|
||||
setSetupData(prev => ({ ...prev, rules: prev.rules.filter(r => r !== rule) }));
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300 capitalize">{rule}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<Server className="w-12 h-12 text-purple-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
MCP Server Setup
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
TaskMaster works best with the MCP server configured
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
MCP Server Configuration
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 mb-3">
|
||||
To enable full TaskMaster integration, add the MCP server configuration to your Claude settings.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded border p-3 mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-mono text-gray-600 dark:text-gray-400">.mcp.json</span>
|
||||
<button
|
||||
onClick={copyMCPConfig}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre className="text-xs text-gray-800 dark:text-gray-200 whitespace-pre-wrap">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": "your_anthropic_key_here",
|
||||
"PERPLEXITY_API_KEY": "your_perplexity_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<a
|
||||
href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use/mcp-servers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
|
||||
>
|
||||
Learn about MCP setup
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Current Status</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
{setupData.mcpConfigured ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm text-green-700 dark:text-green-300">MCP server is configured</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="w-4 h-4 text-amber-500" />
|
||||
<span className="text-sm text-amber-700 dark:text-amber-300">MCP server not detected (optional)</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<FileText className="w-12 h-12 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Product Requirements Document
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Create or import a PRD to generate initial tasks
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
PRD Content
|
||||
</label>
|
||||
<textarea
|
||||
value={setupData.prdContent}
|
||||
onChange={(e) => setSetupData(prev => ({ ...prev, prdContent: e.target.value }))}
|
||||
rows={12}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white font-mono text-sm"
|
||||
placeholder="# Product Requirements Document
|
||||
|
||||
## 1. Overview
|
||||
Describe your project or feature...
|
||||
|
||||
## 2. Objectives
|
||||
- Primary goal
|
||||
- Success metrics
|
||||
|
||||
## 3. User Stories
|
||||
- As a user, I want...
|
||||
|
||||
## 4. Requirements
|
||||
- Feature requirements
|
||||
- Technical requirements
|
||||
|
||||
## 5. Implementation Plan
|
||||
- Phase 1: Core features
|
||||
- Phase 2: Enhancements"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
AI Task Generation
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
TaskMaster will analyze your PRD and automatically generate a structured task list with dependencies, priorities, and implementation details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 4:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Complete Setup
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Ready to initialize TaskMaster for your project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<h4 className="font-medium text-green-900 dark:text-green-100 mb-3">
|
||||
Setup Summary
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm text-green-800 dark:text-green-200">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Project: {setupData.projectRoot}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Rules: {setupData.rules.join(', ')}
|
||||
</li>
|
||||
{setupData.mcpConfigured && (
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
MCP server configured
|
||||
</li>
|
||||
)}
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
PRD content ready ({setupData.prdContent.length} characters)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
What happens next?
|
||||
</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm text-blue-800 dark:text-blue-200">
|
||||
<li>Initialize TaskMaster project structure</li>
|
||||
<li>Save your PRD to <code>.taskmaster/docs/prd.txt</code></li>
|
||||
<li>Generate initial tasks from your PRD</li>
|
||||
<li>Set up project configuration and rules</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-black/50">
|
||||
<div className={cn(
|
||||
'bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 md:rounded-lg shadow-xl',
|
||||
'w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col',
|
||||
className
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sparkles className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
TaskMaster Setup Wizard
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Step {currentStep} of {totalSteps}: {steps[currentStep - 1]?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
title="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="px-4 md:px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<div className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors',
|
||||
currentStep > step.id
|
||||
? 'bg-green-500 text-white'
|
||||
: currentStep === step.id
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
)}>
|
||||
{currentStep > step.id ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
) : (
|
||||
step.id
|
||||
)}
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={cn(
|
||||
'w-16 h-1 mx-2 rounded',
|
||||
currentStep > step.id
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400">
|
||||
{steps.map(step => (
|
||||
<span key={step.id} className="text-center">
|
||||
{step.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
{renderStepContent()}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-red-900 dark:text-red-100 mb-1">Error</h4>
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 md:p-6 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
disabled={currentStep === 1}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{currentStep} of {totalSteps}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
{currentStep === totalSteps ? 'Setting up...' : 'Processing...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{currentStep === totalSteps ? 'Complete Setup' : 'Next'}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskMasterSetupWizard;
|
||||
86
src/components/TaskMasterStatus.jsx
Normal file
86
src/components/TaskMasterStatus.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
|
||||
const TaskMasterStatus = () => {
|
||||
const {
|
||||
currentProject,
|
||||
projectTaskMaster,
|
||||
mcpServerStatus,
|
||||
isLoading,
|
||||
isLoadingMCP,
|
||||
error
|
||||
} = useTaskMaster();
|
||||
|
||||
if (isLoading || isLoadingMCP) {
|
||||
return (
|
||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="animate-spin w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full mr-2"></div>
|
||||
Loading TaskMaster status...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center text-sm text-red-500 dark:text-red-400">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
|
||||
TaskMaster Error
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show MCP server status
|
||||
const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
|
||||
|
||||
// Show project TaskMaster status
|
||||
const projectConfigured = currentProject?.taskmaster?.hasTaskmaster;
|
||||
const taskCount = currentProject?.taskmaster?.metadata?.taskCount || 0;
|
||||
const completedCount = currentProject?.taskmaster?.metadata?.completed || 0;
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="w-2 h-2 bg-gray-400 rounded-full mr-2"></span>
|
||||
No project selected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine overall status for TaskIndicator
|
||||
let overallStatus = 'not-configured';
|
||||
if (projectConfigured && mcpConfigured) {
|
||||
overallStatus = 'fully-configured';
|
||||
} else if (projectConfigured) {
|
||||
overallStatus = 'taskmaster-only';
|
||||
} else if (mcpConfigured) {
|
||||
overallStatus = 'mcp-only';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* TaskMaster Status Indicator */}
|
||||
<TaskIndicator
|
||||
status={overallStatus}
|
||||
size="md"
|
||||
showLabel={true}
|
||||
/>
|
||||
|
||||
{/* Task Progress Info */}
|
||||
{projectConfigured && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">
|
||||
{completedCount}/{taskCount} tasks
|
||||
</span>
|
||||
{taskCount > 0 && (
|
||||
<span className="ml-2 opacity-75">
|
||||
({Math.round((completedCount / taskCount) * 100)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskMasterStatus;
|
||||
@@ -51,9 +51,9 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{todos.map((todo) => (
|
||||
{todos.map((todo, index) => (
|
||||
<div
|
||||
key={todo.id}
|
||||
key={todo.id || `todo-${index}`}
|
||||
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md dark:shadow-gray-900/50 transition-shadow"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
|
||||
@@ -4,9 +4,18 @@ import { Input } from './ui/input';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
|
||||
function ToolsSettings({ isOpen, onClose, projects = [] }) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
const {
|
||||
tasksEnabled,
|
||||
setTasksEnabled,
|
||||
isTaskMasterInstalled,
|
||||
isTaskMasterReady,
|
||||
installationStatus,
|
||||
isCheckingInstallation
|
||||
} = useTasksSettings();
|
||||
const [allowedTools, setAllowedTools] = useState([]);
|
||||
const [disallowedTools, setDisallowedTools] = useState([]);
|
||||
const [newAllowedTool, setNewAllowedTool] = useState('');
|
||||
@@ -630,6 +639,16 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
|
||||
>
|
||||
Appearance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('tasks')}
|
||||
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'tasks'
|
||||
? 'border-blue-600 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
Tasks
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1674,6 +1693,160 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tasks Tab */}
|
||||
{activeTab === 'tasks' && (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
{/* Installation Status Check */}
|
||||
{isCheckingInstallation ? (
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
||||
<span className="text-sm text-muted-foreground">Checking TaskMaster installation...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* TaskMaster Not Installed Warning */}
|
||||
{!isTaskMasterInstalled && (
|
||||
<div className="bg-orange-50 dark:bg-orange-950/50 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-4 h-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100 mb-2">
|
||||
TaskMaster AI CLI Not Installed
|
||||
</div>
|
||||
<div className="text-sm text-orange-800 dark:text-orange-200 space-y-3">
|
||||
<p>TaskMaster CLI is required to use task management features. Install it to get started:</p>
|
||||
|
||||
<div className="bg-orange-100 dark:bg-orange-900/50 rounded-lg p-3 font-mono text-sm">
|
||||
<code>npm install -g task-master-ai</code>
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
View on GitHub
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">After installation:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-xs">
|
||||
<li>Restart this application</li>
|
||||
<li>TaskMaster features will automatically become available</li>
|
||||
<li>Use <code className="bg-orange-100 dark:bg-orange-800 px-1 rounded">task-master init</code> in your project directory</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TaskMaster Settings */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Enable TaskMaster Integration
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
Show TaskMaster tasks, banners, and sidebar indicators across the interface
|
||||
</div>
|
||||
{!isTaskMasterInstalled && (
|
||||
<div className="text-xs text-orange-600 dark:text-orange-400 mt-1">
|
||||
TaskMaster CLI must be installed first
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tasksEnabled}
|
||||
onChange={(e) => setTasksEnabled(e.target.checked)}
|
||||
disabled={!isTaskMasterInstalled}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className={`w-11 h-6 ${!isTaskMasterInstalled ? 'bg-gray-300 dark:bg-gray-600' : 'bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800'} rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600`}></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TaskMaster Information */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Zap className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
🎯 About TaskMaster AI
|
||||
</div>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
|
||||
<p><strong>AI-Powered Task Management:</strong> Break complex projects into manageable subtasks with AI assistance</p>
|
||||
<p><strong>PRD:</strong> Generate structured tasks from Product Requirements Documents</p>
|
||||
<p><strong>Dependency Tracking:</strong> Understand task relationships and execution order</p>
|
||||
<p><strong>Progress Visualization:</strong> Kanban boards, and detailed task views</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Link and Resources */}
|
||||
<div className="mt-4 pt-4 border-t border-blue-200 dark:border-blue-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
📚 Learn More & Tutorial
|
||||
</div>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
|
||||
<p>TaskMaster AI (aka <strong>claude-task-master</strong> ) is an advanced AI-powered task management system built for developers.</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<a
|
||||
href="https://github.com/eyaltoledano/claude-task-master"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
View on GitHub
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
Find documentation, setup guides, and examples for advanced TaskMaster workflows
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
91
src/components/Tooltip.jsx
Normal file
91
src/components/Tooltip.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
position = 'top',
|
||||
className = '',
|
||||
delay = 500
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [timeoutId, setTimeoutId] = useState(null);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const id = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, delay);
|
||||
setTimeoutId(id);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
setTimeoutId(null);
|
||||
}
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const getPositionClasses = () => {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
|
||||
case 'bottom':
|
||||
return 'top-full left-1/2 transform -translate-x-1/2 mt-2';
|
||||
case 'left':
|
||||
return 'right-full top-1/2 transform -translate-y-1/2 mr-2';
|
||||
case 'right':
|
||||
return 'left-full top-1/2 transform -translate-y-1/2 ml-2';
|
||||
default:
|
||||
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
|
||||
}
|
||||
};
|
||||
|
||||
const getArrowClasses = () => {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
|
||||
case 'bottom':
|
||||
return 'bottom-full left-1/2 transform -translate-x-1/2 border-b-gray-900 dark:border-b-gray-100';
|
||||
case 'left':
|
||||
return 'left-full top-1/2 transform -translate-y-1/2 border-l-gray-900 dark:border-l-gray-100';
|
||||
case 'right':
|
||||
return 'right-full top-1/2 transform -translate-y-1/2 border-r-gray-900 dark:border-r-gray-100';
|
||||
default:
|
||||
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
if (!content) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative inline-block"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{children}
|
||||
|
||||
{isVisible && (
|
||||
<div className={cn(
|
||||
'absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-900 dark:bg-gray-100 dark:text-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none',
|
||||
'animate-in fade-in-0 zoom-in-95 duration-200',
|
||||
getPositionClasses(),
|
||||
className
|
||||
)}>
|
||||
{content}
|
||||
|
||||
{/* Arrow */}
|
||||
<div className={cn(
|
||||
'absolute w-0 h-0 border-4 border-transparent',
|
||||
getArrowClasses()
|
||||
)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
Reference in New Issue
Block a user