diff --git a/.gitignore b/.gitignore index e9b1084abb..5d86261500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,22 @@ __pycache__ .env .serena -.claude/settings.local.json PRPs/local PRPs/completed/ /logs/ +.DS_Store +*.log +/tmp/ + +# Claude +.claude/ +claude.local.md +.mcp.json +.next-session.md + +# local build scripts +/scripts/ + .zed tmp/ temp/ diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx index 2a0cdc22f1..1c98e23bf9 100644 --- a/archon-ui-main/src/App.tsx +++ b/archon-ui-main/src/App.tsx @@ -54,9 +54,16 @@ const AppRoutes = () => { <> } /> } /> + } /> + } /> + } /> + } /> ) : ( - } /> + <> + } /> + } /> + )} ); @@ -88,17 +95,17 @@ const AppContent = () => { } }, onReconnected: () => { + console.log('๐Ÿฅ [Health] onReconnected called - clearing disconnect screen'); setDisconnectScreenActive(false); setDisconnectScreenDismissed(false); - // Refresh the page to ensure all data is fresh - window.location.reload(); + // Don't auto-reload - let the user stay on the current page } }); return () => { serverHealthService.stopMonitoring(); }; - }, [disconnectScreenDismissed]); + }, []); // Only run once on mount, not when disconnectScreenDismissed changes const handleDismissDisconnectScreen = () => { setDisconnectScreenActive(false); diff --git a/archon-ui-main/src/components/project-tasks/DocsTab.tsx b/archon-ui-main/src/components/project-tasks/DocsTab.tsx new file mode 100644 index 0000000000..48d72fd461 --- /dev/null +++ b/archon-ui-main/src/components/project-tasks/DocsTab.tsx @@ -0,0 +1,1642 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Plus, X, Search, Upload, Link as LinkIcon, Check, Brain, Save, History, Eye, Edit3, Sparkles, Loader2 } from 'lucide-react'; +import { Button } from '../ui/Button'; +import { knowledgeBaseService, KnowledgeItem } from '../../services/knowledgeBaseService'; +import { projectService } from '../../services/projectService'; +import { useToast } from '../../contexts/ToastContext'; +import { Input } from '../ui/Input'; +import { Card } from '../ui/Card'; +import { Badge } from '../ui/Badge'; +import { Select } from '../ui/Select'; +import { CrawlProgressData, crawlProgressService } from '../../services/crawlProgressService'; +import { WebSocketState } from '../../services/socketIOService'; +import { MilkdownEditor } from './MilkdownEditor'; +import { VersionHistoryModal } from './VersionHistoryModal'; +import { PRPViewer } from '../prp'; +import { DocumentCard, NewDocumentCard } from './DocumentCard'; + + + + +interface ProjectDoc { + id: string; + title: string; + created_at: string; + updated_at: string; + // Content field stores markdown or structured data + content?: any; + document_type?: string; + tags?: string[]; + author?: string; +} + +interface Task { + id: string; + title: string; + feature: string; + status: 'todo' | 'doing' | 'review' | 'done'; +} + +// Document Templates - Updated for proper MCP database storage +const DOCUMENT_TEMPLATES = { + 'prp_base': { + name: 'Feature PRP Template', + icon: '๐Ÿš€', + document_type: 'prp', + content: { + document_type: 'prp', + title: 'New Feature Implementation', + version: '1.0', + author: 'User', + date: new Date().toISOString().split('T')[0], + status: 'draft', + + goal: 'Build a specific feature - replace with your goal', + + why: [ + 'Business value this feature provides', + 'User problem this solves', + 'How it integrates with existing functionality' + ], + + what: { + description: 'Detailed description of what users will see and experience', + success_criteria: [ + 'Measurable outcome 1 (e.g., response time < 200ms)', + 'User behavior outcome 2 (e.g., 90% task completion rate)', + 'Technical outcome 3 (e.g., zero data loss during operations)' + ], + user_stories: [ + 'As a [user type], I want to [action] so that [benefit]', + 'As a [user type], I need to [requirement] in order to [goal]' + ] + }, + + context: { + documentation: [ + { + source: 'https://docs.example.com/api', + why: 'API endpoints and data models needed' + }, + { + source: 'src/components/Example.tsx', + why: 'Existing pattern to follow for UI components' + } + ], + existing_code: [ + { + file: 'src/services/baseService.ts', + purpose: 'Service layer pattern to extend' + } + ], + gotchas: [ + 'Critical requirement or constraint to remember', + 'Common mistake to avoid during implementation' + ], + dependencies: [ + 'Package or service that must be available', + 'Another feature that must be completed first' + ] + }, + + implementation_blueprint: { + phase_1_foundation: { + description: 'Set up core infrastructure', + duration: '2-3 days', + tasks: [ + { + title: 'Create TypeScript interfaces', + details: 'Define all data types and API contracts', + files: ['src/types/newFeature.ts'] + }, + { + title: 'Set up database schema', + details: 'Create tables and relationships if needed', + files: ['migrations/add_feature_tables.sql'] + } + ] + }, + phase_2_implementation: { + description: 'Build core functionality', + duration: '1 week', + tasks: [ + { + title: 'Implement service layer', + details: 'Business logic and data access', + files: ['src/services/newFeatureService.ts'] + }, + { + title: 'Create API endpoints', + details: 'RESTful endpoints with proper validation', + files: ['src/api/newFeatureApi.ts'] + }, + { + title: 'Build UI components', + details: 'React components with TypeScript', + files: ['src/components/NewFeature.tsx'] + } + ] + }, + phase_3_integration: { + description: 'Connect everything and test', + duration: '2-3 days', + tasks: [ + { + title: 'Integrate frontend with backend', + details: 'Connect UI to API endpoints', + files: ['src/hooks/useNewFeature.ts'] + }, + { + title: 'Add comprehensive tests', + details: 'Unit, integration, and E2E tests', + files: ['tests/newFeature.test.ts'] + } + ] + } + }, + + validation: { + level_1_syntax: [ + 'npm run lint -- --fix', + 'npm run typecheck', + 'Ensure no TypeScript errors' + ], + level_2_unit_tests: [ + 'npm run test -- newFeature', + 'Verify all unit tests pass with >80% coverage' + ], + level_3_integration: [ + 'npm run test:integration', + 'Test API endpoints with proper data flow' + ], + level_4_end_to_end: [ + 'Start development server and test user flows', + 'Verify feature works as expected in browser', + 'Test error scenarios and edge cases' + ] + } + } + }, + 'prp_task': { + name: 'Task/Bug Fix PRP', + icon: 'โœ…', + document_type: 'prp', + content: { + document_type: 'prp', + title: 'Task or Bug Fix', + version: '1.0', + author: 'User', + date: new Date().toISOString().split('T')[0], + status: 'draft', + + goal: 'Fix specific bug or complete targeted task', + + why: [ + 'Impact on users or system if not fixed', + 'How this fits into larger project goals', + 'Priority level and urgency' + ], + + what: { + description: 'Specific problem to solve and expected outcome', + current_behavior: 'What happens now (the problem)', + expected_behavior: 'What should happen instead', + acceptance_criteria: [ + 'Specific testable condition 1', + 'Specific testable condition 2', + 'No regressions in existing functionality' + ] + }, + + context: { + affected_files: [ + { + path: 'src/component.tsx', + reason: 'Contains the bug or needs the change' + }, + { + path: 'src/service.ts', + reason: 'Related logic that may need updates' + } + ], + root_cause: 'Analysis of why this issue exists', + related_issues: [ + 'Link to GitHub issue or ticket', + 'Related bugs or enhancement requests' + ], + dependencies: [ + 'Other tasks that must be completed first', + 'External services or APIs involved' + ] + }, + + implementation_steps: [ + { + step: 1, + action: 'Reproduce the issue', + details: 'Create test case that demonstrates the problem' + }, + { + step: 2, + action: 'Identify root cause', + details: 'Debug and trace the issue to its source' + }, + { + step: 3, + action: 'Implement fix', + details: 'Apply minimal change that resolves the issue' + }, + { + step: 4, + action: 'Test solution', + details: 'Verify fix works and doesn\'t break other functionality' + }, + { + step: 5, + action: 'Update documentation', + details: 'Update any relevant docs or comments' + } + ], + + validation: { + reproduction_test: [ + 'Steps to reproduce the original issue', + 'Verify the issue no longer occurs' + ], + regression_tests: [ + 'Run existing test suite to ensure no regressions', + 'Test related functionality manually' + ], + edge_cases: [ + 'Test boundary conditions', + 'Test error scenarios' + ] + } + } + }, + 'prp_planning': { + name: 'Architecture/Planning PRP', + icon: '๐Ÿ“', + document_type: 'prp', + content: { + document_type: 'prp', + title: 'System Architecture and Planning', + version: '1.0', + author: 'User', + date: new Date().toISOString().split('T')[0], + status: 'draft', + + goal: 'Design and plan system architecture for [specific system/feature]', + + why: [ + 'Strategic business objective driving this architecture', + 'Technical debt or scalability issues to address', + 'Future growth and maintainability requirements' + ], + + what: { + scope: 'System boundaries, affected components, and integration points', + deliverables: [ + 'Comprehensive architecture documentation', + 'Component specifications and interfaces', + 'Implementation roadmap and timeline', + 'Migration/deployment strategy' + ], + constraints: [ + 'Budget and timeline limitations', + 'Technical constraints and dependencies', + 'Regulatory or compliance requirements' + ] + }, + + current_state_analysis: { + strengths: [ + 'What works well in the current system', + 'Stable components that should be preserved', + 'Existing patterns worth maintaining' + ], + weaknesses: [ + 'Performance bottlenecks and limitations', + 'Maintenance and scaling challenges', + 'Security or reliability concerns' + ], + opportunities: [ + 'Modern technologies to leverage', + 'Process improvements to implement', + 'Business capabilities to enable' + ], + threats: [ + 'Risks during transition period', + 'Dependencies on legacy systems', + 'Resource and timeline constraints' + ] + }, + + proposed_architecture: { + overview: 'High-level description of the new architecture', + components: { + frontend: { + technology: 'React 18 with TypeScript', + patterns: 'Component composition with ShadCN UI', + state_management: 'React hooks with context for global state' + }, + backend: { + technology: 'FastAPI with async Python', + patterns: 'Service layer with repository pattern', + database: 'Supabase PostgreSQL with proper indexing' + }, + realtime: { + technology: 'Socket.IO for live updates', + patterns: 'Event-driven communication with proper error handling' + }, + infrastructure: { + deployment: 'Docker containers with orchestration', + monitoring: 'Comprehensive logging and metrics', + security: 'OAuth2 with proper encryption' + } + }, + data_flow: [ + 'User interaction โ†’ Frontend validation โ†’ API call', + 'Backend processing โ†’ Database operations โ†’ Response', + 'Real-time events โ†’ Socket.IO โ†’ UI updates' + ], + integration_points: [ + 'External APIs and their usage patterns', + 'Third-party services and data sources', + 'Legacy system interfaces' + ] + }, + + implementation_phases: { + phase_1_foundation: { + duration: '2-3 weeks', + objective: 'Core infrastructure and basic functionality', + deliverables: [ + 'Database schema and basic API endpoints', + 'Authentication and authorization system', + 'Core UI components and routing' + ], + success_criteria: [ + 'Basic user flows working end-to-end', + 'Core API responses under 200ms', + 'Authentication working with test users' + ] + }, + phase_2_features: { + duration: '3-4 weeks', + objective: 'Primary feature implementation', + deliverables: [ + 'Complete feature set with UI', + 'Real-time updates and notifications', + 'Data validation and error handling' + ], + success_criteria: [ + 'All major user stories implemented', + 'Real-time features working reliably', + 'Comprehensive error handling' + ] + }, + phase_3_optimization: { + duration: '1-2 weeks', + objective: 'Testing, optimization, and deployment', + deliverables: [ + 'Comprehensive test suite', + 'Performance optimization', + 'Production deployment' + ], + success_criteria: [ + 'Test coverage >80%', + 'Performance targets met', + 'Successful production deployment' + ] + } + }, + + success_metrics: { + performance: [ + 'API response time <200ms for 95% of requests', + 'UI load time <2 seconds', + 'Support 1000+ concurrent users' + ], + quality: [ + 'Test coverage >80%', + 'Zero critical security vulnerabilities', + 'Mean time to recovery <15 minutes' + ], + business: [ + 'User task completion rate >90%', + 'Feature adoption >60% within first month', + 'User satisfaction score >4.5/5' + ] + }, + + risks_and_mitigation: { + technical_risks: [ + { + risk: 'Integration complexity with legacy systems', + mitigation: 'Phased approach with fallback options' + }, + { + risk: 'Performance issues at scale', + mitigation: 'Load testing and optimization in early phases' + } + ], + business_risks: [ + { + risk: 'Timeline delays due to scope creep', + mitigation: 'Clear requirements and change control process' + } + ] + } + } + }, + + // Simple markdown templates for non-PRP documents + 'markdown_doc': { + name: 'Markdown Document', + icon: '๐Ÿ“', + document_type: 'markdown', + content: { + markdown: `# Document Title + +## Overview + +Provide a brief overview of this document... + +## Content + +Add your content here... + +## Next Steps + +- [ ] Action item 1 +- [ ] Action item 2` + } + }, + + 'meeting_notes': { + name: 'Meeting Notes', + icon: '๐Ÿ“‹', + document_type: 'meeting_notes', + content: { + meeting_date: new Date().toISOString().split('T')[0], + attendees: ['Person 1', 'Person 2'], + agenda: [ + 'Agenda item 1', + 'Agenda item 2' + ], + notes: 'Meeting discussion notes...', + action_items: [ + { + item: 'Action item 1', + owner: 'Person Name', + due_date: 'YYYY-MM-DD' + } + ], + next_meeting: 'YYYY-MM-DD' + } + } +}; + +/* โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” */ +/* Main component */ +/* โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” */ +export const DocsTab = ({ + tasks, + project, + selectedDocumentId, + onDocumentSelect +}: { + tasks: Task[]; + project?: { + id: string; + title: string; + created_at?: string; + updated_at?: string; + } | null; + selectedDocumentId?: string; + onDocumentSelect?: (documentId: string) => void; +}) => { + // Document state + const [documents, setDocuments] = useState([]); + const [selectedDocument, setSelectedDocument] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [loading, setLoading] = useState(false); + const [showTemplateModal, setShowTemplateModal] = useState(false); + const [showVersionHistory, setShowVersionHistory] = useState(false); + const [viewMode, setViewMode] = useState<'beautiful' | 'markdown'>('beautiful'); + + // Dark mode detection + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const checkDarkMode = () => { + const htmlElement = document.documentElement; + const hasDarkClass = htmlElement.classList.contains('dark'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setIsDarkMode(hasDarkClass || prefersDark); + }; + + checkDarkMode(); + + // Listen for changes + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', checkDarkMode); + + return () => { + observer.disconnect(); + mediaQuery.removeEventListener('change', checkDarkMode); + }; + }, []); + + // Knowledge management state + const [showTechnicalModal, setShowTechnicalModal] = useState(false); + const [showBusinessModal, setShowBusinessModal] = useState(false); + const [selectedTechnicalSources, setSelectedTechnicalSources] = useState([]); + const [selectedBusinessSources, setSelectedBusinessSources] = useState([]); + const [showAddSourceModal, setShowAddSourceModal] = useState(false); + const [sourceType, setSourceType] = useState<'technical' | 'business'>('technical'); + const [knowledgeItems, setKnowledgeItems] = useState([]); + const [progressItems, setProgressItems] = useState([]); + const { showToast } = useToast(); + + // Load project documents using light mode for performance + const loadProjectDocuments = async () => { + if (!project?.id) return; + + try { + setLoading(true); + + // Use light mode to get document metadata only (for document cards) + const documentsResponse = await projectService.listDocuments(project.id, false); + + if (!documentsResponse || documentsResponse.length === 0) { + setDocuments([]); + setLoading(false); + return; + } + + // Map to ProjectDoc format for document cards + const projectDocuments: ProjectDoc[] = documentsResponse.map((doc: any) => ({ + id: doc.id, + title: doc.title || 'Untitled Document', + created_at: doc.created_at, + updated_at: doc.updated_at, + content: doc.content || {}, // May be empty in light mode + document_type: doc.document_type || 'document' + })); + + setDocuments(projectDocuments); + + console.log(`Loaded ${projectDocuments.length} documents in light mode`); + } catch (error) { + console.error('Failed to load documents:', error); + showToast('Failed to load documents', 'error'); + } finally { + setLoading(false); + } + }; + + // Load full document content when selecting a specific document + const loadFullDocument = async (docId: string) => { + if (!project?.id || !docId) return; + + // Check if document already has content loaded + const existingDoc = documents.find(d => d.id === docId); + if (existingDoc && existingDoc.content && Object.keys(existingDoc.content).length > 0) { + console.log(`Document ${existingDoc.title} already has content loaded - skipping API call`); + setSelectedDocument(existingDoc); + return; + } + + try { + console.log(`Loading full content for document: ${docId}`); + const fullDoc = await projectService.getDocument(project.id, docId); + + // Build enriched document from existing doc and full content + const enrichedDoc: ProjectDoc = existingDoc + ? { ...existingDoc, content: fullDoc.content || {} } + : { + id: fullDoc.id, + title: fullDoc.title || 'Untitled Document', + created_at: fullDoc.created_at, + updated_at: fullDoc.updated_at, + document_type: fullDoc.document_type || 'document', + content: fullDoc.content || {} + }; + + // Update documents array and selected document with the enriched data + setDocuments(prev => prev.map(doc => doc.id === docId ? enrichedDoc : doc)); + setSelectedDocument(enrichedDoc); + + console.log(`โœ… Loaded full content for document: ${fullDoc.title}`); + } catch (error) { + console.error('Failed to load full document:', error); + showToast('Failed to load document content', 'error'); + } + }; + + // Create new document from template + const createDocumentFromTemplate = async (templateKey: string) => { + if (!project?.id) return; + + const template = DOCUMENT_TEMPLATES[templateKey as keyof typeof DOCUMENT_TEMPLATES]; + if (!template) return; + + try { + setIsSaving(true); + + // Create the document in the database first + const newDocument = await projectService.createDocument(project.id, { + title: template.name, + content: template.content, + document_type: template.document_type, + tags: [] + }); + + // Add to documents list with the real document from the database + setDocuments(prev => [...prev, newDocument]); + setSelectedDocument(newDocument); + + console.log('Document created successfully:', newDocument); + showToast('Document created successfully', 'success'); + setShowTemplateModal(false); + } catch (error) { + console.error('Failed to create document:', error); + showToast( + error instanceof Error ? error.message : 'Failed to create document', + 'error' + ); + } finally { + setIsSaving(false); + } + }; + + // Save document changes + const saveDocument = async () => { + if (!selectedDocument || !project?.id) return; + + try { + setIsSaving(true); + + // Call backend API to persist changes + const updatedDocument = await projectService.updateDocument( + project.id, + selectedDocument.id, + { + title: selectedDocument.title, + content: selectedDocument.content, + tags: selectedDocument.tags, + author: selectedDocument.author + } + ); + + // Update local state with backend response + setDocuments(prev => prev.map(doc => + doc.id === selectedDocument.id ? updatedDocument : doc + )); + setSelectedDocument(updatedDocument); + + console.log('Document saved successfully:', updatedDocument); + showToast('Document saved successfully', 'success'); + setIsEditing(false); + } catch (error) { + console.error('Failed to save document:', error); + showToast( + error instanceof Error ? error.message : 'Failed to save document', + 'error' + ); + } finally { + setIsSaving(false); + } + }; + + // Note: Block editing functions removed - now handled by BlockNoteEditor internally + + // Load project data including linked sources + const loadProjectData = async () => { + if (!project?.id) return; + + try { + const response = await fetch(`/api/projects/${project.id}`); + if (!response.ok) throw new Error('Failed to load project data'); + + const projectData = await response.json(); + + // Initialize selected sources from saved project data + const technicalSourceIds = (projectData.technical_sources || []).map((source: any) => source.source_id); + const businessSourceIds = (projectData.business_sources || []).map((source: any) => source.source_id); + + setSelectedTechnicalSources(technicalSourceIds); + setSelectedBusinessSources(businessSourceIds); + + console.log('Loaded project sources:', { + technical: technicalSourceIds, + business: businessSourceIds + }); + } catch (error) { + console.error('Failed to load project data:', error); + showToast('Failed to load project sources', 'error'); + } + }; + + // Load knowledge items and documents on mount + useEffect(() => { + loadKnowledgeItems(); + loadProjectDocuments(); + loadProjectData(); // Load saved sources + + // Cleanup function to disconnect crawl progress service + return () => { + console.log('๐Ÿงน DocsTab: Disconnecting crawl progress service'); + crawlProgressService.disconnect(); + }; + }, [project?.id]); + + // Clear selected document when project changes + useEffect(() => { + setSelectedDocument(null); + }, [project?.id]); + + // Handle selectedDocumentId from URL - load full content for deep linking + useEffect(() => { + if (selectedDocumentId && documents.length > 0) { + const targetDoc = documents.find(doc => doc.id === selectedDocumentId); + if (targetDoc && targetDoc !== selectedDocument) { + console.log(`๐Ÿ”— URL specified document: ${targetDoc.title} - loading full content`); + loadFullDocument(selectedDocumentId); + setIsEditing(false); + } + } + }, [selectedDocumentId, documents, selectedDocument]); + + // Handle document selection with URL callback - load full content + const handleDocumentSelect = useCallback((document: ProjectDoc) => { + // Load full document content when selected + loadFullDocument(document.id); + if (onDocumentSelect) { + onDocumentSelect(document.id); + } + }, [onDocumentSelect]); + + // Existing knowledge loading function + const loadKnowledgeItems = async (knowledgeType?: 'technical' | 'business') => { + try { + setLoading(true); + const response = await knowledgeBaseService.getKnowledgeItems({ + knowledge_type: knowledgeType, + page: 1, + per_page: 50 + }); + setKnowledgeItems(response.items); + } catch (error) { + console.error('Failed to load knowledge items:', error); + showToast('Failed to load knowledge items', 'error'); + setKnowledgeItems([]); + } finally { + setLoading(false); + } + }; + + // Knowledge management helper functions (simplified for brevity) + const transformToLegacyFormat = (items: KnowledgeItem[]) => { + return items.map(item => ({ + id: item.id, + title: item.title, + type: item.metadata.source_type || 'url', + lastUpdated: new Date(item.updated_at).toLocaleDateString() + })); + }; + + const technicalSources = transformToLegacyFormat( + knowledgeItems.filter(item => item.metadata.knowledge_type === 'technical') + ); + + const businessSources = transformToLegacyFormat( + knowledgeItems.filter(item => item.metadata.knowledge_type === 'business') + ); + + const toggleTechnicalSource = (id: string) => { + setSelectedTechnicalSources(prev => prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]); + }; + const toggleBusinessSource = (id: string) => { + setSelectedBusinessSources(prev => prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]); + }; + const saveTechnicalSources = async () => { + if (!project?.id) return; + + try { + await projectService.updateProject(project.id, { + technical_sources: selectedTechnicalSources + }); + showToast('Technical sources updated successfully', 'success'); + setShowTechnicalModal(false); + // Reload project data to reflect the changes + await loadProjectData(); + } catch (error) { + console.error('Failed to save technical sources:', error); + showToast('Failed to update technical sources', 'error'); + } + }; + + const saveBusinessSources = async () => { + if (!project?.id) return; + + try { + await projectService.updateProject(project.id, { + business_sources: selectedBusinessSources + }); + showToast('Business sources updated successfully', 'success'); + setShowBusinessModal(false); + // Reload project data to reflect the changes + await loadProjectData(); + } catch (error) { + console.error('Failed to save business sources:', error); + showToast('Failed to update business sources', 'error'); + } + }; + + const handleProgressComplete = (data: CrawlProgressData) => { + console.log('Crawl completed:', data); + setProgressItems(prev => prev.filter(item => item.progressId !== data.progressId)); + loadKnowledgeItems(); + showToast('Crawling completed successfully', 'success'); + }; + + const handleProgressError = (error: string) => { + console.error('Crawl error:', error); + showToast(`Crawling failed: ${error}`, 'error'); + }; + + const handleProgressUpdate = (data: CrawlProgressData) => { + setProgressItems(prev => + prev.map(item => + item.progressId === data.progressId ? data : item + ) + ); + }; + + const handleStartCrawl = async (progressId: string, initialData: Partial) => { + console.log(`Starting crawl tracking for: ${progressId}`); + + const newProgressItem: CrawlProgressData = { + progressId, + status: 'starting', + percentage: 0, + logs: ['Starting crawl...'], + ...initialData + }; + + setProgressItems(prev => [...prev, newProgressItem]); + + const progressCallback = (data: CrawlProgressData) => { + console.log(`๐Ÿ“จ Progress callback called for ${progressId}:`, data); + + if (data.progressId === progressId) { + handleProgressUpdate(data); + + if (data.status === 'completed') { + handleProgressComplete(data); + } else if (data.status === 'error') { + handleProgressError(data.error || 'Crawling failed'); + } + } + }; + + try { + // Use the enhanced streamProgress method for better connection handling + await crawlProgressService.streamProgressEnhanced(progressId, { + onMessage: progressCallback, + onError: (error) => { + console.error(`โŒ WebSocket error for ${progressId}:`, error); + handleProgressError(`Connection error: ${error.message}`); + } + }, { + autoReconnect: true, + reconnectDelay: 5000, + connectionTimeout: 10000 + }); + + console.log(`โœ… WebSocket connected successfully for ${progressId}`); + } catch (error) { + console.error(`โŒ Failed to establish WebSocket connection:`, error); + handleProgressError('Failed to connect to progress updates'); + } + }; + + const openAddSourceModal = (type: 'technical' | 'business') => { + setSourceType(type); + setShowAddSourceModal(true); + }; + + return ( +
+
+ {/* Document Header */} +
+
+
+

+ Project Docs +

+

{project?.title || 'No project selected'}

+
+ + {/* View mode and action buttons */} +
+ {selectedDocument && ( +
+ {/* View mode toggle for all documents */} +
+ + +
+ + {isEditing && ( + + )} +
+ )} + + +
+
+
+ + {/* Document Cards Container */} +
+
+ {loading ? ( + // Single loading card following the same pattern as projects + <> +
+
+ + + Loading documents... + +
+
+ {/* Skeleton placeholders */} +
+
+
+
+
+ setShowTemplateModal(true)} /> + + ) : ( + <> + {documents.map(doc => ( + { + try { + // Call API to delete from database first + await projectService.deleteDocument(project.id, docId); + + // Then remove from local state and handle selection atomically + setDocuments(prev => { + const updatedDocuments = prev.filter(d => d.id !== docId); + // If the deleted document was selected, update selection using the new list + if (selectedDocument?.id === docId) { + setSelectedDocument(updatedDocuments.find(d => d.id !== docId) || null); + } + return updatedDocuments; + }); + showToast('Document deleted', 'success'); + } catch (error) { + console.error('Failed to delete document:', error); + showToast('Failed to delete document', 'error'); + } + }} + isDarkMode={isDarkMode} + projectId={project.id} + /> + ))} + + {/* Add New Document Card */} + setShowTemplateModal(true)} /> + + )} +
+
+ + {/* Document Content */} + {loading ? ( +
+
Loading documents...
+
+ ) : selectedDocument ? ( + // Show PRPViewer in beautiful mode for all documents + viewMode === 'beautiful' ? ( +
+ +
+ ) : ( + { + try { + setIsSaving(true); + + // Call backend API to persist changes + const savedDocument = await projectService.updateDocument( + project.id, + updatedDocument.id, + { + title: updatedDocument.title, + content: updatedDocument.content, + tags: updatedDocument.tags, + author: updatedDocument.author + } + ); + + // Update local state with backend response + setSelectedDocument(savedDocument); + setDocuments(prev => prev.map(doc => + doc.id === updatedDocument.id ? savedDocument : doc + )); + + console.log('Document saved via MilkdownEditor'); + showToast('Document saved successfully', 'success'); + } catch (error) { + console.error('Failed to save document:', error); + showToast( + error instanceof Error ? error.message : 'Failed to save document', + 'error' + ); + } finally { + setIsSaving(false); + } + }} + className="mb-8" + /> + ) + ) : ( +
+ +

No documents found

+

Create a new document to get started

+
+ )} + + {/* Knowledge Sections */} +
+ technicalSources.find(source => source.id === id))} + onAddClick={() => setShowTechnicalModal(true)} + /> + businessSources.find(source => source.id === id))} + onAddClick={() => setShowBusinessModal(true)} + /> +
+
+ + {/* Template Selection Modal */} + {showTemplateModal && ( + setShowTemplateModal(false)} + onSelectTemplate={createDocumentFromTemplate} + isCreating={isSaving} + /> + )} + + {/* Existing Modals (simplified for brevity) */} + {showTechnicalModal && ( + setShowTechnicalModal(false)} + onAddSource={() => openAddSourceModal('technical')} + /> + )} + + {showBusinessModal && ( + setShowBusinessModal(false)} + onAddSource={() => openAddSourceModal('business')} + /> + )} + + {showAddSourceModal && ( + setShowAddSourceModal(false)} + onSuccess={() => { + loadKnowledgeItems(); + setShowAddSourceModal(false); + }} + onStartCrawl={handleStartCrawl} + /> + )} + + {/* Version History Modal */} + {showVersionHistory && project && ( + setShowVersionHistory(false)} + onRestore={() => { + // Reload documents after restore + loadProjectDocuments(); + setShowVersionHistory(false); + }} + /> + )} +
+ ); +}; + + +/* โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” */ +/* Helper components */ +/* โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€”โ€” */ + +// ArchonEditor component removed - replaced with BlockNoteEditor + +// Template Modal Component +const TemplateModal: React.FC<{ + onClose: () => void; + onSelectTemplate: (templateKey: string) => void; + isCreating: boolean; +}> = ({ onClose, onSelectTemplate, isCreating }) => { + const templates = Object.entries(DOCUMENT_TEMPLATES); + + const getTemplateDescription = (key: string, template: any) => { + const descriptions: Record = { + 'prp_base': 'Comprehensive template for implementing new features with full context, validation loops, and structured implementation blueprint.', + 'prp_task': 'Focused template for specific tasks or bug fixes with clear steps and validation criteria.', + 'prp_planning': 'Strategic template for architecture planning and system design with risk analysis and success metrics.', + 'markdown_doc': 'Simple markdown document for general documentation and notes.', + 'meeting_notes': 'Structured template for meeting notes with attendees, agenda, and action items.' + }; + return descriptions[key] || 'Document template'; + }; + + return ( +
+
+ +
+
+

+ Choose a Template +

+ +
+ +
+ {templates.map(([key, template]) => ( + + ))} +
+ + {isCreating && ( +
+
+ Creating document... +
+ )} +
+
+
+ ); +}; + +const KnowledgeSection: React.FC<{ + title: string; + color: 'blue' | 'purple' | 'pink' | 'orange'; + sources: any[]; + onAddClick: () => void; +}> = ({ + title, + color, + sources = [], + onAddClick +}) => { + const colorMap = { + blue: { + bg: 'bg-blue-500/10', + border: 'border-blue-500/30', + text: 'text-blue-600 dark:text-blue-400', + buttonBg: 'bg-blue-500/20', + buttonHover: 'hover:bg-blue-500/30', + buttonBorder: 'border-blue-500/40', + buttonShadow: 'hover:shadow-[0_0_15px_rgba(59,130,246,0.3)]', + dot: 'bg-blue-400' + }, + purple: { + bg: 'bg-purple-500/10', + border: 'border-purple-500/30', + text: 'text-purple-600 dark:text-purple-400', + buttonBg: 'bg-purple-500/20', + buttonHover: 'hover:bg-purple-500/30', + buttonBorder: 'border-purple-500/40', + buttonShadow: 'hover:shadow-[0_0_15px_rgba(168,85,247,0.3)]', + dot: 'bg-purple-400' + }, + pink: { + bg: 'bg-pink-500/10', + border: 'border-pink-500/30', + text: 'text-pink-600 dark:text-pink-400', + buttonBg: 'bg-pink-500/20', + buttonHover: 'hover:bg-pink-500/30', + buttonBorder: 'border-pink-500/40', + buttonShadow: 'hover:shadow-[0_0_15px_rgba(236,72,153,0.3)]', + dot: 'bg-pink-400' + }, + orange: { + bg: 'bg-orange-500/10', + border: 'border-orange-500/30', + text: 'text-orange-600 dark:text-orange-400', + buttonBg: 'bg-orange-500/20', + buttonHover: 'hover:bg-orange-500/30', + buttonBorder: 'border-orange-500/40', + buttonShadow: 'hover:shadow-[0_0_15px_rgba(249,115,22,0.3)]', + dot: 'bg-orange-400' + } + }; + return
+
+

+ + {title} +

+ +
+
+
+ {sources && sources.length > 0 ?
+ {sources.map(source => source &&
+ {source.type === 'url' ? : } +
+
+ {source.title} +
+
+ Updated {source.lastUpdated} +
+
+
)} +
:
+

No knowledge sources added yet

+

+ Click "Add Sources" to select relevant documents +

+
} +
+
; +}; + +const SourceSelectionModal: React.FC<{ + title: string; + sources: any[]; + selectedSources: string[]; + onToggleSource: (id: string) => void; + onSave: () => void; + onClose: () => void; + onAddSource: () => void; +}> = ({ + title, + sources, + selectedSources, + onToggleSource, + onSave, + onClose, + onAddSource +}) => { + const [searchQuery, setSearchQuery] = useState(''); + // Filter sources based on search query + const filteredSources = sources.filter(source => source.title.toLowerCase().includes(searchQuery.toLowerCase())); + return
+
+
+
+

+ {title} +

+ +
+ {/* Search and Add Source */} +
+
+ + setSearchQuery(e.target.value)} placeholder="Search sources..." className="w-full bg-white/50 dark:bg-black/70 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded-md py-2 pl-10 pr-3 focus:outline-none focus:border-blue-400 focus:shadow-[0_0_10px_rgba(59,130,246,0.2)] transition-all duration-300" /> +
+ +
+ {/* Sources List */} +
+ {filteredSources.length > 0 ?
+ {filteredSources.map(source =>
onToggleSource(source.id)} className={`flex items-center gap-3 p-3 rounded-md cursor-pointer transition-all duration-200 + ${selectedSources.includes(source.id) ? 'bg-blue-100/80 dark:bg-blue-900/30 border border-blue-300 dark:border-blue-500/50' : 'bg-white/50 dark:bg-black/30 border border-gray-200 dark:border-gray-800 hover:border-gray-300 dark:hover:border-gray-700'}`}> +
+ {selectedSources.includes(source.id) && } +
+ {source.type === 'url' ? : } +
+
+ {source.title} +
+
+ Updated {source.lastUpdated} +
+
+
)} +
:
+ No sources found matching your search +
} +
+ {/* Action Buttons */} +
+ + +
+
+
+
; +}; + +interface AddKnowledgeModalProps { + sourceType: 'technical' | 'business'; + onClose: () => void; + onSuccess: () => void; + onStartCrawl: (progressId: string, initialData: Partial) => void; +} + +const AddKnowledgeModal = ({ + sourceType, + onClose, + onSuccess, + onStartCrawl +}: AddKnowledgeModalProps) => { + const [method, setMethod] = useState<'url' | 'file'>('url'); + const [url, setUrl] = useState(''); + const [updateFrequency, setUpdateFrequency] = useState('7'); + const [tags, setTags] = useState([]); + const [newTag, setNewTag] = useState(''); + const [selectedFile, setSelectedFile] = useState(null); + const [loading, setLoading] = useState(false); + const { showToast } = useToast(); + + const handleSubmit = async () => { + try { + setLoading(true); + + if (method === 'url') { + if (!url.trim()) { + showToast('Please enter a URL', 'error'); + return; + } + + const result = await knowledgeBaseService.crawlUrl({ + url: url.trim(), + knowledge_type: sourceType, + tags, + update_frequency: parseInt(updateFrequency) + }); + + // Check if result contains a progressId for streaming + if ((result as any).progressId) { + // Start progress tracking + onStartCrawl((result as any).progressId, { + currentUrl: url.trim(), + totalPages: 0, + processedPages: 0 + }); + + showToast('Crawling started - tracking progress', 'success'); + onClose(); // Close modal immediately + } else { + // Fallback for non-streaming response + showToast((result as any).message || 'Crawling started', 'success'); + onSuccess(); + } + } else { + if (!selectedFile) { + showToast('Please select a file', 'error'); + return; + } + + const result = await knowledgeBaseService.uploadDocument(selectedFile, { + knowledge_type: sourceType, + tags + }); + + showToast((result as any).message || 'Document uploaded successfully', 'success'); + onSuccess(); + } + } catch (error) { + console.error('Failed to add knowledge:', error); + showToast('Failed to add knowledge source', 'error'); + } finally { + setLoading(false); + } + }; + + return
+ +

+ Add {sourceType === 'technical' ? 'Technical' : 'Business'} Knowledge Source +

+ + {/* Source Type Selection */} +
+ + +
+ + {/* URL Input */} + {method === 'url' &&
+ setUrl(e.target.value)} placeholder="https://..." accentColor="blue" /> +
} + + {/* File Upload */} + {method === 'file' &&
+ + setSelectedFile(e.target.files?.[0] || null)} + className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100" + /> +

+ Supports PDF, MD, DOC up to 10MB +

+
} + + {/* Update Frequency */} + {method === 'url' &&
+ setNewTag(e.target.value)} onKeyDown={e => { + if (e.key === 'Enter' && newTag.trim()) { + setTags([...tags, newTag.trim()]); + setNewTag(''); + } + }} placeholder="Add tags..." accentColor="purple" /> +
+ + {/* Action Buttons */} +
+ + +
+
+
; +}; \ No newline at end of file diff --git a/archon-ui-main/src/components/project-tasks/DocumentCard.tsx b/archon-ui-main/src/components/project-tasks/DocumentCard.tsx new file mode 100644 index 0000000000..b8784b3278 --- /dev/null +++ b/archon-ui-main/src/components/project-tasks/DocumentCard.tsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { Rocket, Code, Briefcase, Users, FileText, X, Plus, Clipboard, ExternalLink } from 'lucide-react'; +import { useToast } from '../../contexts/ToastContext'; +import { handleCopyClick, copyUrlToClipboard } from '../../utils/copyHelpers'; +import { needsCopyLinkButton } from '../../utils/platformDetection'; + +export interface ProjectDoc { + id: string; + title: string; + content: any; + document_type?: string; + updated_at: string; + created_at?: string; +} + +interface DocumentCardProps { + document: ProjectDoc; + isActive: boolean; + onSelect: (doc: ProjectDoc) => void; + onDelete: (docId: string) => void; + isDarkMode: boolean; + projectId: string; +} + +export const DocumentCard: React.FC = ({ + document, + isActive, + onSelect, + onDelete, + isDarkMode, + projectId +}) => { + const [showDelete, setShowDelete] = useState(false); + const { showToast } = useToast(); + + const getDocumentIcon = (type?: string) => { + switch (type) { + case 'prp': return ; + case 'technical': return ; + case 'business': return ; + case 'meeting_notes': return ; + default: return ; + } + }; + + const getTypeColor = (type?: string) => { + switch (type) { + case 'prp': return 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30'; + case 'technical': return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30'; + case 'business': return 'bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30'; + case 'meeting_notes': return 'bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/30'; + default: return 'bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/30'; + } + }; + + const handleCopyId = async (e: React.MouseEvent) => { + e.stopPropagation(); + + // Capture button reference before async call + const button = e.currentTarget as HTMLButtonElement; + const originalHTML = button.innerHTML; + + try { + const result = await handleCopyClick(e, 'document', projectId, document.id); + + if (result.success) { + const message = result.copied === 'url' + ? 'Document URL copied to clipboard' + : 'Document ID copied to clipboard'; + showToast(message, 'success'); + + // Visual feedback + button.innerHTML = '
โœ“Copied
'; + setTimeout(() => { + button.innerHTML = originalHTML; + }, 2000); + } else { + showToast('Failed to copy to clipboard', 'error'); + } + } catch (error) { + console.error('Exception in copy handler:', error); + showToast('Failed to copy to clipboard', 'error'); + } + }; + + return ( +
onSelect(document)} + onMouseEnter={() => setShowDelete(true)} + onMouseLeave={() => setShowDelete(false)} + > + {/* Document Type Badge */} +
+ {getDocumentIcon(document.document_type)} + {document.document_type || 'document'} +
+ + {/* Title */} +

+ {document.title} +

+ + {/* Metadata */} +

+ {new Date(document.updated_at || document.created_at || Date.now()).toLocaleDateString()} +

+ + {/* ID Display Section - Always visible for active, hover for others, and always visible on mobile */} +
+ + {document.id.slice(0, 8)}... + +
+ {/* Enhanced Copy Document ID Button with shift-click support */} + + + {/* Mobile Copy Link Button - shown on iOS/Android */} + {needsCopyLinkButton() && ( + + )} +
+
+ + {/* Delete Button */} + {showDelete && !isActive && ( + + )} +
+ ); +}; + +// New Document Card Component +interface NewDocumentCardProps { + onClick: () => void; +} + +export const NewDocumentCard: React.FC = ({ onClick }) => { + return ( +
+ + New Document +
+ ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx b/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx new file mode 100644 index 0000000000..d53be1a916 --- /dev/null +++ b/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx @@ -0,0 +1,323 @@ +import React, { useRef, useState } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { Edit, Trash2, RefreshCw, Tag, User, Bot, Clipboard, ExternalLink } from 'lucide-react'; +import { Task } from './TaskTableView'; +import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils'; +import { handleCopyClick, copyUrlToClipboard } from '../../utils/copyHelpers'; +import { needsCopyLinkButton } from '../../utils/platformDetection'; +import { useToast } from '../../contexts/ToastContext'; + +export interface DraggableTaskCardProps { + task: Task; + index: number; + onView: () => void; + onComplete: () => void; + onDelete: (task: Task) => void; + onTaskReorder: (taskId: string, targetIndex: number, status: Task['status']) => void; + tasksInStatus: Task[]; + allTasks?: Task[]; + hoveredTaskId?: string | null; + onTaskHover?: (taskId: string | null) => void; + selectedTaskId?: string; + projectId: string; + currentView?: 'table' | 'board'; +} + +export const DraggableTaskCard = ({ + task, + index, + onView, + onDelete, + onTaskReorder, + allTasks = [], + hoveredTaskId, + onTaskHover, + selectedTaskId, + projectId, + currentView = 'board' +}: DraggableTaskCardProps) => { + const { showToast } = useToast(); + + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.TASK, + item: { id: task.id, status: task.status, index }, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging() + }) + }); + + const [, drop] = useDrop({ + accept: ItemTypes.TASK, + hover: (draggedItem: { id: string; status: Task['status']; index: number }, monitor) => { + if (!monitor.isOver({ shallow: true })) return; + if (draggedItem.id === task.id) return; + if (draggedItem.status !== task.status) return; + + const draggedIndex = draggedItem.index; + const hoveredIndex = index; + + if (draggedIndex === hoveredIndex) return; + + console.log('BOARD HOVER: Moving task', draggedItem.id, 'from index', draggedIndex, 'to', hoveredIndex, 'in status', task.status); + + // Move the task immediately for visual feedback (same pattern as table view) + onTaskReorder(draggedItem.id, hoveredIndex, task.status); + + // Update the dragged item's index to prevent re-triggering + draggedItem.index = hoveredIndex; + } + }); + + const [isFlipped, setIsFlipped] = useState(false); + + const toggleFlip = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsFlipped(!isFlipped); + }; + + // Calculate hover effects for parent-child relationships + const getRelatedTaskIds = () => { + const relatedIds = new Set(); + + return relatedIds; + }; + + const relatedTaskIds = getRelatedTaskIds(); + const isHighlighted = hoveredTaskId ? relatedTaskIds.has(hoveredTaskId) || hoveredTaskId === task.id : false; + + const handleMouseEnter = () => { + onTaskHover?.(task.id); + }; + + const handleMouseLeave = () => { + onTaskHover?.(null); + }; + + + // Card styling - using CSS-based height animation for better scrolling + + // Card styling constants + const cardScale = 'scale-100'; + const cardOpacity = 'opacity-100'; + + // Highlight for selected task (blue) and related tasks (cyan) + const selectedHighlight = selectedTaskId === task.id + ? 'bg-gradient-to-br from-blue-100/90 to-purple-100/90 dark:from-blue-900/50 dark:to-purple-900/50 border-blue-400/70 dark:border-blue-500/60 shadow-[0_0_15px_rgba(59,130,246,0.3)]' + : ''; + + // Subtle highlight effect for related tasks - applied to the card, not parent + const highlightGlow = isHighlighted + ? 'border-cyan-400/50 shadow-[0_0_8px_rgba(34,211,238,0.2)]' + : ''; + + // Simplified hover effect - just a glowing border + const hoverEffectClasses = 'group-hover:border-cyan-400/70 dark:group-hover:border-cyan-500/50 group-hover:shadow-[0_0_15px_rgba(34,211,238,0.4)] dark:group-hover:shadow-[0_0_15px_rgba(34,211,238,0.6)]'; + + // Base card styles with proper rounded corners + const cardBaseStyles = 'bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border border-gray-200 dark:border-gray-700 rounded-lg'; + + // Transition settings + const transitionStyles = 'transition-all duration-200 ease-in-out'; + + return ( +
drag(drop(node))} + data-task-id={task.id} + style={{ + perspective: '1000px', + transformStyle: 'preserve-3d' + }} + className={`flip-card w-full min-h-[140px] cursor-move relative ${cardScale} ${cardOpacity} ${isDragging ? 'opacity-50 scale-90' : ''} ${transitionStyles} group`} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > +
+ {/* Front side with subtle hover effect */} +
+ {/* Priority indicator */} +
+ + {/* Content container with fixed padding - exactly matching back side structure */} +
+
+
+ + {task.feature} +
+ + {/* Task order display */} +
+ {task.task_order} +
+ + {/* Action buttons group */} +
+ + + +
+
+ +

+ {task.title} +

+ + {/* Spacer to push assignee section to bottom */} +
+ +
+
+
+ {getAssigneeIcon(task.assignee?.name || 'User')} +
+ {task.assignee?.name || 'User'} +
+ +
+ {/* Enhanced Copy Task ID Button with shift-click support */} + + + {/* Mobile Copy Link Button - shown on iOS/Android */} + {needsCopyLinkButton() && ( + + )} +
+
+
+
+ + {/* Back side */} + {/* Back side with same hover effect */} +
+ {/* Priority indicator */} +
+ + {/* Content container with fixed padding */} +
+
+

+ {task.title} +

+ +
+ + {/* Description container with absolute positioning inside parent bounds */} +
+
+

{task.description}

+
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx b/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx new file mode 100644 index 0000000000..cb0ebefdff --- /dev/null +++ b/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx @@ -0,0 +1,474 @@ +import React, { useRef, useState, useCallback, useEffect } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { useToast } from '../../contexts/ToastContext'; +import { DeleteConfirmModal } from '../../pages/ProjectPage'; +import { CheckSquare, Square, Trash2, ArrowRight } from 'lucide-react'; +import { projectService } from '../../services/projectService'; +import { Task } from './TaskTableView'; // Import Task interface +import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils'; +import { DraggableTaskCard, DraggableTaskCardProps } from './DraggableTaskCard'; // Import the new component and its props + +interface TaskBoardViewProps { + tasks: Task[]; + onTaskView: (task: Task) => void; + onTaskComplete: (taskId: string) => void; + onTaskDelete: (task: Task) => void; + onTaskMove: (taskId: string, newStatus: Task['status']) => void; + onTaskReorder: (taskId: string, targetIndex: number, status: Task['status']) => void; + selectedTaskId?: string; + projectId: string; + currentView?: 'table' | 'board'; +} + +interface ColumnDropZoneProps { + status: Task['status']; + title: string; + tasks: Task[]; + onTaskMove: (taskId: string, newStatus: Task['status']) => void; + onTaskView: (task: Task) => void; + onTaskComplete: (taskId: string) => void; + onTaskDelete: (task: Task) => void; + onTaskReorder: (taskId: string, targetIndex: number, status: Task['status']) => void; + allTasks: Task[]; + hoveredTaskId: string | null; + selectedTaskId?: string; + onTaskHover: (taskId: string | null) => void; + selectedTasks: Set; + onTaskSelect: (taskId: string) => void; + projectId: string; + currentView?: 'table' | 'board'; +} + +const ColumnDropZone = ({ + status, + title, + tasks, + onTaskMove, + onTaskView, + onTaskComplete, + onTaskDelete, + onTaskReorder, + allTasks, + hoveredTaskId, + selectedTaskId, + onTaskHover, + selectedTasks, + onTaskSelect, + projectId, + currentView +}: ColumnDropZoneProps) => { + const ref = useRef(null); + + const [{ isOver }, drop] = useDrop({ + accept: ItemTypes.TASK, + drop: (item: { id: string; status: string }) => { + if (item.status !== status) { + // Moving to different status - use length of current column as new order + onTaskMove(item.id, status); + } + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver() + }) + }); + + drop(ref); + + // Get column header color based on status + const getColumnColor = () => { + switch (status) { + case 'backlog': + return 'text-gray-600 dark:text-gray-400'; + case 'in-progress': + return 'text-blue-600 dark:text-blue-400'; + case 'review': + return 'text-purple-600 dark:text-purple-400'; + case 'complete': + return 'text-green-600 dark:text-green-400'; + } + }; + + // Get column header glow based on status + const getColumnGlow = () => { + switch (status) { + case 'backlog': + return 'bg-gray-500/30'; + case 'in-progress': + return 'bg-blue-500/30 shadow-[0_0_10px_2px_rgba(59,130,246,0.2)]'; + case 'review': + return 'bg-purple-500/30 shadow-[0_0_10px_2px_rgba(168,85,247,0.2)]'; + case 'complete': + return 'bg-green-500/30 shadow-[0_0_10px_2px_rgba(16,185,129,0.2)]'; + } + }; + + // Just use the tasks as-is since they're already parent tasks only + const organizedTasks = tasks; + + return ( +
+
+

{title}

+ {/* Column header divider with glow */} +
+
+ +
+ {organizedTasks.map((task, index) => ( + onTaskView(task)} + onComplete={() => onTaskComplete(task.id)} + onDelete={onTaskDelete} + onTaskReorder={onTaskReorder} + tasksInStatus={organizedTasks} + allTasks={allTasks} + hoveredTaskId={hoveredTaskId} + onTaskHover={onTaskHover} + selectedTaskId={selectedTaskId} + projectId={projectId} + currentView={currentView} + /> + ))} +
+
+ ); +}; + +export const TaskBoardView = ({ + tasks, + onTaskView, + onTaskComplete, + onTaskDelete, + onTaskMove, + onTaskReorder, + selectedTaskId, + projectId, + currentView = 'board' +}: TaskBoardViewProps) => { + const [hoveredTaskId, setHoveredTaskId] = useState(null); + const [selectedTasks, setSelectedTasks] = useState>(new Set()); + + // State for delete confirmation modal + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [taskToDelete, setTaskToDelete] = useState(null); + + const { showToast } = useToast(); + + // Auto-scroll selected task into view (following projects pattern) + useEffect(() => { + if (selectedTaskId && tasks.length > 0) { + // Small delay to ensure DOM is updated + setTimeout(() => { + const taskCard = document.querySelector(`[data-task-id="${selectedTaskId}"]`); + + if (taskCard) { + // Find the column scroll container (the parent with overflow-y-auto) + let scrollContainer = taskCard.parentElement; + while (scrollContainer && !scrollContainer.classList.contains('overflow-y-auto')) { + scrollContainer = scrollContainer.parentElement; + } + + + if (scrollContainer) { + // Get the position of the card relative to the scroll container + const containerScrollTop = scrollContainer.scrollTop; + const containerHeight = scrollContainer.clientHeight; + + // Get card position relative to scroll container using getBoundingClientRect + const containerRect = scrollContainer.getBoundingClientRect(); + const cardRect = taskCard.getBoundingClientRect(); + const cardOffsetTop = cardRect.top - containerRect.top + containerScrollTop; + const cardHeight = taskCard.clientHeight; + + // Calculate the scroll position to center the card + const targetScrollTop = Math.max(0, cardOffsetTop - (containerHeight / 2) + (cardHeight / 2)); + + + // Store initial scroll position to verify movement + const initialScrollTop = scrollContainer.scrollTop; + + // Check if scroll is actually needed + if (Math.abs(targetScrollTop - initialScrollTop) < 5) { + return; + } + + // Smooth scroll to center the selected task + scrollContainer.scrollTo({ + top: targetScrollTop, + behavior: 'smooth' + }); + + } + } + }, 300); // Small delay to ensure DOM is updated + } + }, [selectedTaskId, tasks]); + + // Multi-select handlers + const toggleTaskSelection = useCallback((taskId: string) => { + setSelectedTasks(prev => { + const newSelection = new Set(prev); + if (newSelection.has(taskId)) { + newSelection.delete(taskId); + } else { + newSelection.add(taskId); + } + return newSelection; + }); + }, []); + + const selectAllTasks = useCallback(() => { + setSelectedTasks(new Set(tasks.map(task => task.id))); + }, [tasks]); + + const clearSelection = useCallback(() => { + setSelectedTasks(new Set()); + }, []); + + // Mass delete handler + const handleMassDelete = useCallback(async () => { + if (selectedTasks.size === 0) return; + + const tasksToDelete = tasks.filter(task => selectedTasks.has(task.id)); + + try { + // Delete all selected tasks + await Promise.all( + tasksToDelete.map(task => projectService.deleteTask(task.id)) + ); + + // Clear selection + clearSelection(); + + showToast(`${tasksToDelete.length} tasks deleted successfully`, 'success'); + } catch (error) { + console.error('Failed to delete tasks:', error); + showToast('Failed to delete some tasks', 'error'); + } + }, [selectedTasks, tasks, clearSelection, showToast]); + + // Mass status change handler + const handleMassStatusChange = useCallback(async (newStatus: Task['status']) => { + if (selectedTasks.size === 0) return; + + const tasksToUpdate = tasks.filter(task => selectedTasks.has(task.id)); + + try { + // Update all selected tasks + await Promise.all( + tasksToUpdate.map(task => + projectService.updateTask(task.id, { + status: mapUIStatusToDBStatus(newStatus) + }) + ) + ); + + // Clear selection + clearSelection(); + + showToast(`${tasksToUpdate.length} tasks moved to ${newStatus}`, 'success'); + } catch (error) { + console.error('Failed to update tasks:', error); + showToast('Failed to update some tasks', 'error'); + } + }, [selectedTasks, tasks, clearSelection, showToast]); + + // Helper function to map UI status to DB status (reuse from TasksTab) + const mapUIStatusToDBStatus = (uiStatus: Task['status']) => { + switch (uiStatus) { + case 'backlog': return 'todo'; + case 'in-progress': return 'doing'; + case 'review': return 'review'; + case 'complete': return 'done'; + default: return 'todo'; + } + }; + + // Handle task deletion (opens confirmation modal) + const handleDeleteTask = useCallback((task: Task) => { + setTaskToDelete(task); + setShowDeleteConfirm(true); + }, [setTaskToDelete, setShowDeleteConfirm]); + + // Confirm deletion and execute + const confirmDeleteTask = useCallback(async () => { + if (!taskToDelete) return; + + try { + await projectService.deleteTask(taskToDelete.id); + // Notify parent to update tasks + onTaskDelete(taskToDelete); + showToast(`Task "${taskToDelete.title}" deleted successfully`, 'success'); + } catch (error) { + console.error('Failed to delete task:', error); + showToast(error instanceof Error ? error.message : 'Failed to delete task', 'error'); + } finally { + setShowDeleteConfirm(false); + setTaskToDelete(null); + } + }, [taskToDelete, onTaskDelete, showToast, setShowDeleteConfirm, setTaskToDelete, projectService]); + + // Cancel deletion + const cancelDeleteTask = useCallback(() => { + setShowDeleteConfirm(false); + setTaskToDelete(null); + }, [setShowDeleteConfirm, setTaskToDelete]); + + // Simple task filtering for board view + const getTasksByStatus = (status: Task['status']) => { + return tasks + .filter(task => task.status === status) + .sort((a, b) => a.task_order - b.task_order); + }; + + return ( +
+ {/* Multi-select toolbar */} + {selectedTasks.size > 0 && ( +
+
+ + {selectedTasks.size} task{selectedTasks.size !== 1 ? 's' : ''} selected + +
+ +
+ {/* Status change dropdown */} + + + {/* Mass delete button */} + + + {/* Clear selection */} + +
+
+ )} + + {/* Board Columns */} +
+ {/* Backlog Column */} + + + {/* In Progress Column */} + + + {/* Review Column */} + + + {/* Complete Column */} + +
+ + {/* Delete Confirmation Modal for Tasks */} + {showDeleteConfirm && taskToDelete && ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/components/project-tasks/TaskTableView.tsx b/archon-ui-main/src/components/project-tasks/TaskTableView.tsx new file mode 100644 index 0000000000..464d50cec2 --- /dev/null +++ b/archon-ui-main/src/components/project-tasks/TaskTableView.tsx @@ -0,0 +1,1010 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus, ExternalLink } from 'lucide-react'; +import { useToast } from '../../contexts/ToastContext'; +import { DeleteConfirmModal } from '../../pages/ProjectPage'; +import { projectService } from '../../services/projectService'; +import { handleCopyClick, copyUrlToClipboard } from '../../utils/copyHelpers'; +import { needsCopyLinkButton } from '../../utils/platformDetection'; +import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils'; +import { DraggableTaskCard } from './DraggableTaskCard'; + +export interface Task { + id: string; + title: string; + description: string; + status: 'backlog' | 'in-progress' | 'review' | 'complete'; + assignee: { + name: 'User' | 'Archon' | 'AI IDE Agent'; + avatar: string; + }; + feature: string; + featureColor: string; + task_order: number; +} + +interface TaskTableViewProps { + tasks: Task[]; + onTaskView: (task: Task) => void; + onTaskComplete: (taskId: string) => void; + onTaskDelete: (task: Task) => void; + onTaskReorder: (taskId: string, newOrder: number, status: Task['status']) => void; + onTaskCreate?: (task: Omit) => Promise; + onTaskUpdate?: (taskId: string, updates: Partial) => Promise; + selectedTaskId?: string; + projectId: string; + currentView?: 'table' | 'board'; +} + +const getAssigneeGlassStyle = (assigneeName: 'User' | 'Archon' | 'AI IDE Agent') => { + switch (assigneeName) { + case 'User': + return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-blue-400 dark:border-blue-500'; // blue glass + case 'AI IDE Agent': + return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-emerald-400 dark:border-emerald-500'; // emerald green glass (like toggle) + case 'Archon': + return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-pink-400 dark:border-pink-500'; // pink glass + default: + return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-blue-400 dark:border-blue-500'; + } +}; + +// Get glass morphism style based on task order (lower = higher priority = warmer color) +const getOrderGlassStyle = (order: number) => { + if (order <= 3) return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-rose-400 dark:border-rose-500'; // red glass + if (order <= 6) return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-orange-400 dark:border-orange-500'; // orange glass + if (order <= 10) return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-blue-400 dark:border-blue-500'; // blue glass + return 'backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border-emerald-400 dark:border-emerald-500'; // green glass +}; + +const getOrderTextColor = (order: number) => { + if (order <= 3) return 'text-rose-500 dark:text-rose-400'; // red text + if (order <= 6) return 'text-orange-500 dark:text-orange-400'; // orange text + if (order <= 10) return 'text-blue-500 dark:text-blue-400'; // blue text + return 'text-emerald-500 dark:text-emerald-400'; // green text +}; + + + +// Helper function to reorder tasks properly +const reorderTasks = (tasks: Task[], fromIndex: number, toIndex: number): Task[] => { + const result = [...tasks]; + const [movedTask] = result.splice(fromIndex, 1); + result.splice(toIndex, 0, movedTask); + + // Update task_order to be sequential (1, 2, 3, ...) + return result.map((task, index) => ({ + ...task, + task_order: index + 1 + })); +}; + +// Inline editable cell component +interface EditableCellProps { + value: string; + onSave: (value: string) => void; + type?: 'text' | 'textarea' | 'select'; + options?: string[]; + placeholder?: string; + isEditing: boolean; + onEdit: () => void; + onCancel: () => void; +} + +const EditableCell = ({ + value, + onSave, + type = 'text', + options = [], + placeholder = '', + isEditing, + onEdit, + onCancel +}: EditableCellProps) => { + const [editValue, setEditValue] = useState(value); + + const handleSave = () => { + onSave(editValue); + }; + + const handleCancel = () => { + setEditValue(value); + onCancel(); + }; + + // Handle keyboard events for Tab/Enter to save, Escape to cancel + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault(); + handleSave(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancel(); + } + }; + + // Handle blur to save (when clicking outside) + const handleBlur = () => { + handleSave(); + }; + + if (!isEditing) { + return ( +
+ + {value || Click to edit} + +
+ ); + } + + return ( +
+ {type === 'select' ? ( + + ) : type === 'textarea' ? ( +