diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json index 831b1a92..c32001e0 100644 --- a/archon-ui-main/package-lock.json +++ b/archon-ui-main/package-lock.json @@ -12,6 +12,7 @@ "@milkdown/kit": "^7.5.0", "@milkdown/plugin-history": "^7.5.0", "@milkdown/preset-commonmark": "^7.5.0", + "@types/uuid": "^10.0.0", "@xyflow/react": "^12.3.0", "clsx": "latest", "date-fns": "^4.1.0", @@ -26,6 +27,7 @@ "react-router-dom": "^6.26.2", "socket.io-client": "^4.8.1", "tailwind-merge": "latest", + "uuid": "^11.1.0", "zod": "^3.25.46" }, "devDependencies": { @@ -2977,6 +2979,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -10025,6 +10033,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/archon-ui-main/package.json b/archon-ui-main/package.json index fc6a1d1a..70fa7136 100644 --- a/archon-ui-main/package.json +++ b/archon-ui-main/package.json @@ -22,6 +22,7 @@ "@milkdown/kit": "^7.5.0", "@milkdown/plugin-history": "^7.5.0", "@milkdown/preset-commonmark": "^7.5.0", + "@types/uuid": "^10.0.0", "@xyflow/react": "^12.3.0", "clsx": "latest", "date-fns": "^4.1.0", @@ -36,6 +37,7 @@ "react-router-dom": "^6.26.2", "socket.io-client": "^4.8.1", "tailwind-merge": "latest", + "uuid": "^11.1.0", "zod": "^3.25.46" }, "devDependencies": { diff --git a/archon-ui-main/src/components/ErrorBoundary.tsx b/archon-ui-main/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..81f2337e --- /dev/null +++ b/archon-ui-main/src/components/ErrorBoundary.tsx @@ -0,0 +1,319 @@ +/** + * Error Boundary Component with React 18 Features + * Provides fallback UI and error recovery options + */ + +import React, { Component, ErrorInfo, ReactNode, Suspense } from 'react'; +import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; + errorCount: number; +} + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: (error: Error, errorInfo: ErrorInfo, reset: () => void) => ReactNode; + onError?: (error: Error, errorInfo: ErrorInfo) => void; + resetKeys?: Array; + resetOnPropsChange?: boolean; + isolate?: boolean; + level?: 'page' | 'section' | 'component'; +} + +/** + * Enhanced Error Boundary with recovery options + */ +export class ErrorBoundary extends Component { + private resetTimeoutId: NodeJS.Timeout | null = null; + private previousResetKeys: Array = []; + + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + errorCount: 0 + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { + hasError: true, + error + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + const { onError } = this.props; + + // Log error details + console.error('Error caught by boundary:', error); + console.error('Error info:', errorInfo); + + // Update state with error details + this.setState(prevState => ({ + errorInfo, + errorCount: prevState.errorCount + 1 + })); + + // Call error handler if provided + if (onError) { + onError(error, errorInfo); + } + + // In alpha, we want to fail fast and require explicit user action + // Log detailed error information for debugging + console.error('[ErrorBoundary] Component error caught:', { + error: error.toString(), + stack: error.stack, + componentStack: errorInfo.componentStack, + errorCount: this.state.errorCount + 1, + isolate: this.props.isolate + }); + } + + componentDidUpdate(prevProps: ErrorBoundaryProps): void { + const { resetKeys, resetOnPropsChange } = this.props; + const { hasError } = this.state; + + // Reset on prop changes if enabled + if (hasError && prevProps.children !== this.props.children && resetOnPropsChange) { + this.reset(); + } + + // Reset on resetKeys change + if (hasError && resetKeys && this.previousResetKeys !== resetKeys) { + const hasResetKeyChanged = resetKeys.some( + (key, index) => key !== this.previousResetKeys[index] + ); + + if (hasResetKeyChanged) { + this.reset(); + } + } + + this.previousResetKeys = resetKeys || []; + } + + componentWillUnmount(): void { + if (this.resetTimeoutId) { + clearTimeout(this.resetTimeoutId); + this.resetTimeoutId = null; + } + } + + reset = (): void => { + if (this.resetTimeoutId) { + clearTimeout(this.resetTimeoutId); + this.resetTimeoutId = null; + } + + this.setState({ + hasError: false, + error: null, + errorInfo: null + }); + }; + + render(): ReactNode { + const { hasError, error, errorInfo, errorCount } = this.state; + const { children, fallback, level = 'component' } = this.props; + + if (hasError && error && errorInfo) { + // Use custom fallback if provided + if (fallback) { + return fallback(error, errorInfo, this.reset); + } + + // Default fallback UI based on level + return ; + } + + return children; + } +} + +/** + * Default error fallback component + */ +interface DefaultErrorFallbackProps { + error: Error; + errorInfo: ErrorInfo; + reset: () => void; + level: 'page' | 'section' | 'component'; + errorCount: number; +} + +const DefaultErrorFallback: React.FC = ({ + error, + errorInfo, + reset, + level, + errorCount +}) => { + const isPageLevel = level === 'page'; + const isSectionLevel = level === 'section'; + + if (level === 'component') { + // Minimal component-level error + return ( +
+
+ + + Component error occurred + + +
+
+ ); + } + + return ( +
+
+
+ {/* Error Icon */} +
+
+ +
+
+ + {/* Error Title */} +

+ {isPageLevel ? 'Something went wrong' : 'An error occurred'} +

+ + {/* Error Message */} +

+ {error.message || 'An unexpected error occurred while rendering this component.'} +

+ + {/* Retry Count */} + {errorCount > 1 && ( +

+ This error has occurred {errorCount} times +

+ )} + + {/* Action Buttons */} +
+ + + {isPageLevel && ( + + )} +
+ + {/* Error Details (Development Only) */} + {process.env.NODE_ENV === 'development' && ( +
+ + Error Details (Development Only) + +
+
+

+ {error.stack} +

+
+
+

+ Component Stack: +

+

+ {errorInfo.componentStack} +

+
+
+
+ )} +
+
+
+ ); +}; + +/** + * Suspense Error Boundary - combines Suspense with Error Boundary + */ +interface SuspenseErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; + errorFallback?: (error: Error, errorInfo: ErrorInfo, reset: () => void) => ReactNode; + level?: 'page' | 'section' | 'component'; +} + +export const SuspenseErrorBoundary: React.FC = ({ + children, + fallback, + errorFallback, + level = 'component' +}) => { + const defaultFallback = ( +
+
+
+ ); + + return ( + + + {children} + + + ); +}; + +/** + * Hook to reset error boundaries + */ +export function useErrorHandler(): (error: Error) => void { + return (error: Error) => { + throw error; + }; +} \ No newline at end of file diff --git a/archon-ui-main/src/components/SearchableList.tsx b/archon-ui-main/src/components/SearchableList.tsx new file mode 100644 index 00000000..9c3b370d --- /dev/null +++ b/archon-ui-main/src/components/SearchableList.tsx @@ -0,0 +1,365 @@ +/** + * SearchableList Component with React 18 Concurrent Features + * Uses useTransition for non-blocking search updates + */ + +import React, { useState, useTransition, useMemo, useCallback } from 'react'; +import { Search, X, Loader2 } from 'lucide-react'; + +export interface SearchableListItem { + id: string; + title: string; + description?: string; + metadata?: Record; +} + +export interface SearchableListProps { + items: T[]; + onItemClick?: (item: T) => void; + onItemSelect?: (item: T) => void; + renderItem?: (item: T, isHighlighted: boolean) => React.ReactNode; + searchFields?: (keyof T)[]; + placeholder?: string; + emptyMessage?: string; + className?: string; + itemClassName?: string; + enableMultiSelect?: boolean; + selectedItems?: T[]; + virtualize?: boolean; + virtualizeThreshold?: number; + // Virtualization configuration + itemHeight?: number; // Height of each item in pixels (default: 80) + containerHeight?: number; // Height of scrollable container in pixels (default: 600) +} + +/** + * SearchableList with React 18 concurrent features + */ +export function SearchableList({ + items, + onItemClick, + onItemSelect, + renderItem, + searchFields = ['title', 'description'] as (keyof T)[], + placeholder = 'Search...', + emptyMessage = 'No items found', + className = '', + itemClassName = '', + enableMultiSelect = false, + selectedItems = [], + virtualize = true, + virtualizeThreshold = 100, + itemHeight = 80, + containerHeight = 600 +}: SearchableListProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [highlightedId, setHighlightedId] = useState(null); + const [selectedIds, setSelectedIds] = useState>( + new Set(selectedItems.map(item => item.id)) + ); + + // Use transition for non-blocking search updates + const [isPending, startTransition] = useTransition(); + + /** + * Filter items based on search query with transition + */ + const filteredItems = useMemo(() => { + if (!searchQuery.trim()) { + return items; + } + + const query = searchQuery.toLowerCase(); + return items.filter(item => { + return searchFields.some(field => { + const value = item[field]; + if (typeof value === 'string') { + return value.toLowerCase().includes(query); + } + if (value && typeof value === 'object') { + return JSON.stringify(value).toLowerCase().includes(query); + } + return false; + }); + }); + }, [items, searchQuery, searchFields]); + + /** + * Handle search input with transition + */ + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + + // Use transition for non-urgent update + startTransition(() => { + setSearchQuery(value); + }); + }, []); + + /** + * Clear search + */ + const handleClearSearch = useCallback(() => { + startTransition(() => { + setSearchQuery(''); + }); + }, []); + + /** + * Handle item selection + */ + const handleItemSelect = useCallback((item: T) => { + if (enableMultiSelect) { + setSelectedIds(prev => { + const next = new Set(prev); + if (next.has(item.id)) { + next.delete(item.id); + } else { + next.add(item.id); + } + return next; + }); + } else { + setSelectedIds(new Set([item.id])); + } + + if (onItemSelect) { + onItemSelect(item); + } + }, [enableMultiSelect, onItemSelect]); + + /** + * Handle item click + */ + const handleItemClick = useCallback((item: T) => { + if (onItemClick) { + onItemClick(item); + } else { + handleItemSelect(item); + } + }, [onItemClick, handleItemSelect]); + + /** + * Default item renderer + */ + const defaultRenderItem = useCallback((item: T, isHighlighted: boolean) => { + const isSelected = selectedIds.has(item.id); + + return ( +
setHighlightedId(item.id)} + onMouseLeave={() => setHighlightedId(null)} + onClick={() => handleItemClick(item)} + > +
+
+

+ {item.title} +

+ {item.description && ( +

+ {item.description} +

+ )} +
+ {enableMultiSelect && ( + handleItemSelect(item)} + onClick={(e) => e.stopPropagation()} + className="ml-3 mt-1 h-4 w-4 text-blue-600 rounded focus:ring-blue-500" + /> + )} +
+
+ ); + }, [selectedIds, itemClassName, handleItemClick, handleItemSelect, enableMultiSelect]); + + /** + * Virtualized list renderer for large lists + */ + const [scrollTop, setScrollTop] = useState(0); + + const renderVirtualizedList = useCallback(() => { + // Simple virtualization with configurable dimensions + const visibleCount = Math.ceil(containerHeight / itemHeight); + + const startIndex = Math.floor(scrollTop / itemHeight); + const endIndex = Math.min(startIndex + visibleCount + 1, filteredItems.length); + const visibleItems = filteredItems.slice(startIndex, endIndex); + const totalHeight = filteredItems.length * itemHeight; + const offsetY = startIndex * itemHeight; + + return ( +
setScrollTop(e.currentTarget.scrollTop)} + > +
+
+ {visibleItems.map(item => ( +
+ {renderItem ? renderItem(item, highlightedId === item.id) : defaultRenderItem(item, highlightedId === item.id)} +
+ ))} +
+
+
+ ); + }, [filteredItems, highlightedId, renderItem, defaultRenderItem, containerHeight, itemHeight, scrollTop]); + + /** + * Regular list renderer + */ + const renderRegularList = useCallback(() => { + return ( +
+ {filteredItems.map(item => ( +
+ {renderItem ? renderItem(item, highlightedId === item.id) : defaultRenderItem(item, highlightedId === item.id)} +
+ ))} +
+ ); + }, [filteredItems, highlightedId, renderItem, defaultRenderItem]); + + return ( +
+ {/* Search Bar */} +
+
+ +
+ {isPending ? ( + + ) : ( + + )} +
+ {searchQuery && ( + + )} +
+ {isPending && ( +
+ Searching... +
+ )} +
+ + {/* Results Count */} + {searchQuery && ( +
+ {filteredItems.length} result{filteredItems.length !== 1 ? 's' : ''} found +
+ )} + + {/* List Container */} +
+ {filteredItems.length === 0 ? ( +
+ {emptyMessage} +
+ ) : ( + <> + {virtualize && filteredItems.length > virtualizeThreshold + ? renderVirtualizedList() + : renderRegularList() + } + + )} +
+ + {/* Selection Summary */} + {enableMultiSelect && selectedIds.size > 0 && ( +
+

+ {selectedIds.size} item{selectedIds.size !== 1 ? 's' : ''} selected +

+
+ )} +
+ ); +} + +/** + * Hook for managing searchable list state + */ +export function useSearchableList( + items: T[], + searchFields: (keyof T)[] = ['title', 'description'] as (keyof T)[] +) { + const [searchQuery, setSearchQuery] = useState(''); + const [isPending, startTransition] = useTransition(); + + const filteredItems = useMemo(() => { + if (!searchQuery.trim()) { + return items; + } + + const query = searchQuery.toLowerCase(); + return items.filter(item => { + return searchFields.some(field => { + const value = item[field]; + if (typeof value === 'string') { + return value.toLowerCase().includes(query); + } + return false; + }); + }); + }, [items, searchQuery, searchFields]); + + const updateSearch = useCallback((query: string) => { + startTransition(() => { + setSearchQuery(query); + }); + }, []); + + const clearSearch = useCallback(() => { + startTransition(() => { + setSearchQuery(''); + }); + }, []); + + return { + searchQuery, + filteredItems, + isPending, + updateSearch, + clearSearch + }; +} \ No newline at end of file diff --git a/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx b/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx index ede71170..f4fc88f8 100644 --- a/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx +++ b/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx @@ -149,7 +149,7 @@ export const KnowledgeItemCard = ({ const [showPageTooltip, setShowPageTooltip] = useState(false); const [isRemoving, setIsRemoving] = useState(false); const [showEditModal, setShowEditModal] = useState(false); - const [loadedCodeExamples, setLoadedCodeExamples] = useState(null); + const [loadedCodeExamples, setLoadedCodeExamples] = useState | null>(null); const [isLoadingCodeExamples, setIsLoadingCodeExamples] = useState(false); const statusColorMap = { diff --git a/archon-ui-main/src/components/project-tasks/DocsTab.tsx b/archon-ui-main/src/components/project-tasks/DocsTab.tsx index 87e6fa5c..075ebb89 100644 --- a/archon-ui-main/src/components/project-tasks/DocsTab.tsx +++ b/archon-ui-main/src/components/project-tasks/DocsTab.tsx @@ -601,21 +601,18 @@ export const DocsTab = ({ try { setIsSaving(true); - // Create a new document with a unique ID - const newDocument: ProjectDoc = { - id: `doc-${Date.now()}`, + // Create document via backend API + const newDocument = await projectService.createDocument(project.id, { title: template.name, content: template.content, - document_type: template.document_type, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }; + document_type: template.document_type + }); // Add to documents list setDocuments(prev => [...prev, newDocument]); setSelectedDocument(newDocument); - console.log('Document created successfully:', newDocument); + console.log('Document created successfully via API:', newDocument); showToast('Document created successfully', 'success'); setShowTemplateModal(false); } catch (error) { @@ -636,18 +633,19 @@ export const DocsTab = ({ try { setIsSaving(true); - // Update the document in local state - const updatedDocument = { - ...selectedDocument, - updated_at: new Date().toISOString() - }; + // Update the document via backend API + const updatedDocument = await projectService.updateDocument(project.id, selectedDocument.id, { + ...selectedDocument, + updated_at: new Date().toISOString() + }); + // Update local state with the response from backend setDocuments(prev => prev.map(doc => doc.id === selectedDocument.id ? updatedDocument : doc )); setSelectedDocument(updatedDocument); - console.log('Document saved successfully:', updatedDocument); + console.log('Document saved successfully via API:', updatedDocument); showToast('Document saved successfully', 'success'); setIsEditing(false); } catch (error) { @@ -938,6 +936,8 @@ export const DocsTab = ({ isActive={selectedDocument?.id === doc.id} onSelect={setSelectedDocument} onDelete={async (docId) => { + if (!project?.id) return; + try { // Call API to delete from database first await projectService.deleteDocument(project.id, docId); @@ -981,22 +981,24 @@ export const DocsTab = ({ document={selectedDocument} isDarkMode={isDarkMode} onSave={async (updatedDocument) => { + if (!project?.id) return; + try { setIsSaving(true); - // Update document with timestamp - const docWithTimestamp = { + // Update document via backend API + const savedDocument = await projectService.updateDocument(project.id, updatedDocument.id, { ...updatedDocument, updated_at: new Date().toISOString() - }; + }); - // Update local state - setSelectedDocument(docWithTimestamp); + // Update local state with the response from backend + setSelectedDocument(savedDocument); setDocuments(prev => prev.map(doc => - doc.id === updatedDocument.id ? docWithTimestamp : doc + doc.id === updatedDocument.id ? savedDocument : doc )); - console.log('Document saved via MilkdownEditor'); + console.log('Document saved via MilkdownEditor API:', savedDocument); showToast('Document saved successfully', 'success'); } catch (error) { console.error('Failed to save document:', error); @@ -1190,7 +1192,7 @@ const TemplateModal: React.FC<{ const KnowledgeSection: React.FC<{ title: string; color: 'blue' | 'purple' | 'pink' | 'orange'; - sources: any[]; + sources: Array<{id: string; title: string; type: string; lastUpdated: string} | undefined>; onAddClick: () => void; }> = ({ title, @@ -1273,7 +1275,7 @@ const KnowledgeSection: React.FC<{ const SourceSelectionModal: React.FC<{ title: string; - sources: any[]; + sources: Array<{id: string; title: string; type: string; lastUpdated: string}>; selectedSources: string[]; onToggleSource: (id: string) => void; onSave: () => void; diff --git a/archon-ui-main/src/components/project-tasks/DocumentCard.tsx b/archon-ui-main/src/components/project-tasks/DocumentCard.tsx index 5ef45a56..26966e9b 100644 --- a/archon-ui-main/src/components/project-tasks/DocumentCard.tsx +++ b/archon-ui-main/src/components/project-tasks/DocumentCard.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { Rocket, Code, Briefcase, Users, FileText, X, Plus, Clipboard } from 'lucide-react'; import { useToast } from '../../contexts/ToastContext'; +import { copyToClipboard } from '../../utils/clipboard'; export interface ProjectDoc { id: string; @@ -49,18 +50,22 @@ export const DocumentCard: React.FC = ({ } }; - const handleCopyId = (e: React.MouseEvent) => { + const handleCopyId = async (e: React.MouseEvent) => { e.stopPropagation(); - navigator.clipboard.writeText(document.id); - showToast('Document ID copied to clipboard', 'success'); - - // Visual feedback - const button = e.currentTarget; - const originalHTML = button.innerHTML; - button.innerHTML = '
Copied
'; - setTimeout(() => { - button.innerHTML = originalHTML; - }, 2000); + const success = await copyToClipboard(document.id); + if (success) { + showToast('Document ID copied to clipboard', 'success'); + + // Visual feedback + const button = e.currentTarget; + const originalHTML = button.innerHTML; + button.innerHTML = '
Copied
'; + setTimeout(() => { + button.innerHTML = originalHTML; + }, 2000); + } else { + showToast('Failed to copy Document ID', 'error'); + } }; return ( diff --git a/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx b/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx index b632dea7..166148a4 100644 --- a/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx +++ b/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx @@ -3,6 +3,8 @@ import { useDrag, useDrop } from 'react-dnd'; import { Edit, Trash2, RefreshCw, Tag, User, Bot, Clipboard } from 'lucide-react'; import { Task } from './TaskTableView'; import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils'; +import { copyToClipboard } from '../../utils/clipboard'; +import { useToast } from '../../contexts/ToastContext'; export interface DraggableTaskCardProps { task: Task; @@ -27,6 +29,7 @@ export const DraggableTaskCard = ({ hoveredTaskId, onTaskHover, }: DraggableTaskCardProps) => { + const { showToast } = useToast(); const [{ isDragging }, drag] = useDrag({ type: ItemTypes.TASK, @@ -188,16 +191,21 @@ export const DraggableTaskCard = ({ {task.assignee?.name || 'User'} {/* Copy Task ID Button - Matching Board View */}