diff --git a/.env.example b/.env.example
index dc00d2e6c3..4077e9cd22 100644
--- a/.env.example
+++ b/.env.example
@@ -42,6 +42,12 @@ ARCHON_DOCS_PORT=3838
# If not set, defaults to localhost, 127.0.0.1, ::1, and the HOST value above
VITE_ALLOWED_HOSTS=
+# Development Tools
+# VITE_SHOW_DEVTOOLS: Show TanStack Query DevTools (for developers only)
+# Set to "true" to enable the DevTools panel in bottom right corner
+# Defaults to "false" for end users
+VITE_SHOW_DEVTOOLS=false
+
# When enabled, PROD mode will proxy ARCHON_SERVER_PORT through ARCHON_UI_PORT. This exposes both the
# Archon UI and API through a single port. This is useful when deploying Archon behind a reverse
# proxy where you want to expose the frontend on a single external domain.
diff --git a/.gitignore b/.gitignore
index 5145d4dc35..bf0230adf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ __pycache__
PRPs/local
PRPs/completed/
/logs/
+.zed
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..3c0928c168
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,388 @@
+# AGENTS.md
+
+## Alpha Development Guidelines
+
+**Local-only deployment** - each user runs their own instance.
+
+### Core Principles
+
+- **No backwards compatibility** - remove deprecated code immediately
+- **Detailed errors over graceful failures** - we want to identify and fix issues fast
+- **Break things to improve them** - alpha is for rapid iteration
+
+### Error Handling
+
+**Core Principle**: In alpha, we need to intelligently decide when to fail hard and fast to quickly address issues, and when to allow processes to complete in critical services despite failures. Read below carefully and make intelligent decisions on a case-by-case basis.
+
+#### When to Fail Fast and Loud (Let it Crash!)
+
+These errors should stop execution and bubble up immediately: (except for crawling flows)
+
+- **Service startup failures** - If credentials, database, or any service can't initialize, the system should crash with a clear error
+- **Missing configuration** - Missing environment variables or invalid settings should stop the system
+- **Database connection failures** - Don't hide connection issues, expose them
+- **Authentication/authorization failures** - Security errors must be visible and halt the operation
+- **Data corruption or validation errors** - Never silently accept bad data, Pydantic should raise
+- **Critical dependencies unavailable** - If a required service is down, fail immediately
+- **Invalid data that would corrupt state** - Never store zero embeddings, null foreign keys, or malformed JSON
+
+#### When to Complete but Log Detailed Errors
+
+These operations should continue but track and report failures clearly:
+
+- **Batch processing** - When crawling websites or processing documents, complete what you can and report detailed failures for each item
+- **Background tasks** - Embedding generation, async jobs should finish the queue but log failures
+- **WebSocket events** - Don't crash on a single event failure, log it and continue serving other clients
+- **Optional features** - If projects/tasks are disabled, log and skip rather than crash
+- **External API calls** - Retry with exponential backoff, then fail with a clear message about what service failed and why
+
+#### Critical Nuance: Never Accept Corrupted Data
+
+When a process should continue despite failures, it must **skip the failed item entirely** rather than storing corrupted data:
+
+**❌ WRONG - Silent Corruption:**
+
+```python
+try:
+ embedding = create_embedding(text)
+except Exception as e:
+ embedding = [0.0] * 1536 # NEVER DO THIS - corrupts database
+ store_document(doc, embedding)
+```
+
+**✅ CORRECT - Skip Failed Items:**
+
+```python
+try:
+ embedding = create_embedding(text)
+ store_document(doc, embedding) # Only store on success
+except Exception as e:
+ failed_items.append({'doc': doc, 'error': str(e)})
+ logger.error(f"Skipping document {doc.id}: {e}")
+ # Continue with next document, don't store anything
+```
+
+**✅ CORRECT - Batch Processing with Failure Tracking:**
+
+```python
+def process_batch(items):
+ results = {'succeeded': [], 'failed': []}
+
+ for item in items:
+ try:
+ result = process_item(item)
+ results['succeeded'].append(result)
+ except Exception as e:
+ results['failed'].append({
+ 'item': item,
+ 'error': str(e),
+ 'traceback': traceback.format_exc()
+ })
+ logger.error(f"Failed to process {item.id}: {e}")
+
+ # Always return both successes and failures
+ return results
+```
+
+#### Error Message Guidelines
+
+- Include context about what was being attempted when the error occurred
+- Preserve full stack traces with `exc_info=True` in Python logging
+- Use specific exception types, not generic Exception catching
+- Include relevant IDs, URLs, or data that helps debug the issue
+- Never return None/null to indicate failure - raise an exception with details
+- For batch operations, always report both success count and detailed failure list
+
+### Code Quality
+
+- Remove dead code immediately rather than maintaining it - no backward compatibility or legacy functions
+- Prioritize functionality over production-ready patterns
+- Focus on user experience and feature completeness
+- When updating code, don't reference what is changing (avoid keywords like LEGACY, CHANGED, REMOVED), instead focus on comments that document just the functionality of the code
+
+## Architecture Overview
+
+Archon V2 Alpha is a microservices-based knowledge management system with MCP (Model Context Protocol) integration:
+
+- **Frontend (port 3737)**: React + TypeScript + Vite + TailwindCSS
+ - **UI Strategy**: Radix UI primitives in `/features`, custom components in legacy `/components`
+ - **State Management**: TanStack Query for all data fetching in `/features`
+ - **Styling**: Tron-inspired glassmorphism with Tailwind CSS
+- **Main Server (port 8181)**: FastAPI with HTTP polling for updates
+- **MCP Server (port 8051)**: Lightweight HTTP-based MCP protocol server
+- **Agents Service (port 8052)**: PydanticAI agents for AI/ML operations
+- **Database**: Supabase (PostgreSQL + pgvector for embeddings)
+
+## Development Commands
+
+### Frontend (archon-ui-main/)
+
+```bash
+npm run dev # Start development server on port 3737
+npm run build # Build for production
+npm run lint # Run ESLint
+npm run test # Run Vitest tests
+npm run test:coverage # Run tests with coverage report
+```
+
+# Biome Linter Guide for AI Assistants
+
+## Overview
+
+This project uses Biome for linting and formatting the `/src/features` directory. Biome provides fast, machine-readable feedback that AI assistants can use to improve code quality.
+
+## Configuration
+
+Biome is configured in `biome.json`:
+
+- **Scope**: Only checks `/src/features/**` directory
+- **Formatting**: 2 spaces, 80 char line width
+- **Linting**: Recommended rules enabled
+- **Import Organization**: Automatically sorts and groups imports
+
+## AI Assistant Workflow in the new /features directory
+
+1. **Check Issues**: Run `npm run biome:ai` to get JSON output
+2. **Parse Output**: Extract error locations and types
+3. **Apply Fixes**:
+ - Run `npm run biome:ai-fix` for auto-fixable issues
+ - Manually fix remaining issues based on patterns above
+4. **Verify**: Run `npm run biome:ai` again to confirm fixes
+
+## JSON Output Format
+
+When using `biome:ai`, the output is structured JSON:
+
+```json
+{
+ "diagnostics": [
+ {
+ "file": "path/to/file.tsx",
+ "line": 10,
+ "column": 5,
+ "severity": "error",
+ "message": "Description of the issue",
+ "rule": "lint/a11y/useButtonType"
+ }
+ ]
+}
+```
+
+### Backend (python/)
+
+```bash
+# Using uv package manager
+uv sync # Install/update dependencies
+uv run pytest # Run tests
+uv run python -m src.server.main # Run server locally
+
+# With Docker
+docker-compose up --build -d # Start all services
+docker-compose logs -f # View logs
+docker-compose restart # Restart services
+```
+
+### Testing
+
+```bash
+# Frontend tests (from archon-ui-main/)
+npm run test:coverage:stream # Run with streaming output
+npm run test:ui # Run with Vitest UI
+
+# Backend tests (from python/)
+uv run pytest tests/test_api_essentials.py -v
+uv run pytest tests/test_service_integration.py -v
+```
+
+## Key API Endpoints
+
+### Knowledge Base
+
+- `POST /api/knowledge/crawl` - Crawl a website
+- `POST /api/knowledge/upload` - Upload documents (PDF, DOCX, MD)
+- `GET /api/knowledge/items` - List knowledge items
+- `POST /api/knowledge/search` - RAG search
+
+### MCP Integration
+
+- `GET /api/mcp/health` - MCP server status
+- `POST /api/mcp/tools/{tool_name}` - Execute MCP tool
+- `GET /api/mcp/tools` - List available tools
+
+### Projects & Tasks (when enabled)
+
+- `GET /api/projects` - List all projects
+- `POST /api/projects` - Create project
+- `GET /api/projects/{id}` - Get single project
+- `PUT /api/projects/{id}` - Update project
+- `DELETE /api/projects/{id}` - Delete project
+- `GET /api/projects/{id}/tasks` - Get tasks for project (use this, not getTasks)
+- `POST /api/tasks` - Create task
+- `PUT /api/tasks/{id}` - Update task
+- `DELETE /api/tasks/{id}` - Delete task
+
+## Polling Architecture
+
+### HTTP Polling (replaced Socket.IO)
+
+- **Polling intervals**: 1-2s for active operations, 5-10s for background data
+- **ETag caching**: Reduces bandwidth by ~70% via 304 Not Modified responses
+- **Smart pausing**: Stops polling when browser tab is inactive
+- **Progress endpoints**: `/api/progress/crawl`, `/api/progress/project-creation`
+
+### Key Polling Hooks
+
+- `usePolling` - Generic polling with ETag support
+- `useDatabaseMutation` - Optimistic updates with rollback
+- `useProjectMutation` - Project-specific operations
+
+## Environment Variables
+
+Required in `.env`:
+
+```bash
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_SERVICE_KEY=your-service-key-here
+```
+
+Optional:
+
+```bash
+OPENAI_API_KEY=your-openai-key # Can be set via UI
+LOGFIRE_TOKEN=your-logfire-token # For observability
+LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
+```
+
+## File Organization
+
+### Frontend Structure
+
+- `src/components/` - Legacy UI components (custom-built)
+- `src/features/` - Modern vertical slice architecture with Radix UI
+ - `ui/primitives/` - Radix UI primitives with Tron glassmorphism
+ - `projects/` - Project management feature
+ - `tasks/` - Task management sub-feature
+- `src/pages/` - Main application pages
+- `src/services/` - API communication and business logic
+- `src/hooks/` - Custom React hooks
+- `src/contexts/` - React context providers
+
+### UI Libraries
+
+- **Radix UI** (@radix-ui/react-\*) - Unstyled, accessible primitives for `/features`
+- **TanStack Query** - Data fetching and caching for `/features`
+- **React DnD** - Drag and drop for Kanban boards
+- **Tailwind CSS** - Utility-first styling with Tron-inspired glassmorphism
+- **Framer Motion** - Animations (minimal usage)
+
+### Theme Management
+
+- **ThemeContext** - Manages light/dark theme state
+- **Tailwind dark mode** - Uses `dark:` prefix with selector strategy
+- **Automatic switching** - All components respect theme via Tailwind classes
+- **Persistent** - Theme choice saved in localStorage
+- **Tron aesthetic** - Stronger neon glows in dark mode, subtle in light mode
+
+We're migrating to a vertical slice architecture where each feature is self-contained. Features are organized by domain hierarchy - main features contain their sub-features. For example, tasks are a sub-feature of projects, so they live at `features/projects/tasks/` rather than as separate siblings. Each feature level has its own components, hooks, types, and services folders. This keeps related code together and makes the codebase more maintainable as it scales.
+
+### Backend Structure
+
+- `src/server/` - Main FastAPI application
+- `src/server/api_routes/` - API route handlers
+- `src/server/services/` - Business logic services
+- `src/mcp/` - MCP server implementation
+- `src/agents/` - PydanticAI agent implementations
+
+## Database Schema
+
+Key tables in Supabase:
+
+- `sources` - Crawled websites and uploaded documents
+- `documents` - Processed document chunks with embeddings
+- `projects` - Project management (optional feature)
+- `tasks` - Task tracking linked to projects
+- `code_examples` - Extracted code snippets
+
+## API Naming Conventions
+
+### Task Status Values
+
+Use database values directly (no UI mapping):
+
+- `todo`, `doing`, `review`, `done`
+
+### Service Method Patterns
+
+- `get[Resource]sByProject(projectId)` - Scoped queries
+- `get[Resource](id)` - Single resource
+- `create[Resource](data)` - Create operations
+- `update[Resource](id, updates)` - Updates
+- `delete[Resource](id)` - Soft deletes
+
+### State Naming
+
+- `is[Action]ing` - Loading states (e.g., `isSwitchingProject`)
+- `[resource]Error` - Error messages
+- `selected[Resource]` - Current selection
+
+## Common Development Tasks
+
+### Add a new API endpoint
+
+1. Create route handler in `python/src/server/api_routes/`
+2. Add service logic in `python/src/server/services/`
+3. Include router in `python/src/server/main.py`
+4. Update frontend service in `archon-ui-main/src/services/`
+
+### Add a new UI component
+
+For **features** directory (preferred for new components):
+
+1. Use Radix UI primitives from `src/features/ui/primitives/`
+2. Create component in relevant feature folder under `src/features/`
+3. Use TanStack Query for data fetching
+4. Apply Tron-inspired glassmorphism styling with Tailwind
+
+For **legacy** components:
+
+1. Create component in `archon-ui-main/src/components/`
+2. Add to page in `archon-ui-main/src/pages/`
+3. Include any new API calls in services
+4. Add tests in `archon-ui-main/test/`
+
+### Debug MCP connection issues
+
+1. Check MCP health: `curl http://localhost:8051/health`
+2. View MCP logs: `docker-compose logs archon-mcp`
+3. Test tool execution via UI MCP page
+4. Verify Supabase connection and credentials
+
+## Code Quality Standards
+
+We enforce code quality through automated linting and type checking:
+
+- **Python 3.12** with 120 character line length
+- **Ruff** for linting - checks for errors, warnings, unused imports, and code style
+- **Mypy** for type checking - ensures type safety across the codebase
+- **Auto-formatting** on save in IDEs to maintain consistent style
+- Run `uv run ruff check` and `uv run mypy src/` locally before committing
+
+## MCP Tools Available
+
+When connected to Cursor/Windsurf:
+
+- `archon:perform_rag_query` - Search knowledge base
+- `archon:search_code_examples` - Find code snippets
+- `archon:manage_project` - Project operations
+- `archon:manage_task` - Task management
+- `archon:get_available_sources` - List knowledge sources
+
+## Important Notes
+
+- Projects feature is optional - toggle in Settings UI
+- All services communicate via HTTP, not gRPC
+- HTTP polling handles all updates (Socket.IO removed)
+- Frontend uses Vite proxy for API calls in development
+- Python backend uses `uv` for dependency management
+- Docker Compose handles service orchestration
+- we use tanstack query NO PROP DRILLING! refacring in progress!
diff --git a/CLAUDE.md b/CLAUDE.md
index 23808e55b2..b618b16137 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,7 +18,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
#### When to Fail Fast and Loud (Let it Crash!)
-These errors should stop execution and bubble up immediately:
+These errors should stop execution and bubble up immediately: (except for crawling flows)
- **Service startup failures** - If credentials, database, or any service can't initialize, the system should crash with a clear error
- **Missing configuration** - Missing environment variables or invalid settings should stop the system
@@ -102,16 +102,6 @@ def process_batch(items):
- Focus on user experience and feature completeness
- When updating code, don't reference what is changing (avoid keywords like LEGACY, CHANGED, REMOVED), instead focus on comments that document just the functionality of the code
-## Architecture Overview
-
-Archon V2 Alpha is a microservices-based knowledge management system with MCP (Model Context Protocol) integration:
-
-- **Frontend (port 3737)**: React + TypeScript + Vite + TailwindCSS
-- **Main Server (port 8181)**: FastAPI with HTTP polling for updates
-- **MCP Server (port 8051)**: Lightweight HTTP-based MCP protocol server
-- **Agents Service (port 8052)**: PydanticAI agents for AI/ML operations
-- **Database**: Supabase (PostgreSQL + pgvector for embeddings)
-
## Development Commands
### Frontend (archon-ui-main/)
@@ -119,129 +109,231 @@ Archon V2 Alpha is a microservices-based knowledge management system with MCP (M
```bash
npm run dev # Start development server on port 3737
npm run build # Build for production
-npm run lint # Run ESLint
-npm run test # Run Vitest tests
-npm run test:coverage # Run tests with coverage report
+npm run lint # Run ESLint on legacy code (excludes /features)
+npm run lint:files path/to/file.tsx # Lint specific files
+
+# Biome for /src/features directory only
+npm run biome # Check features directory
+npm run biome:fix # Auto-fix issues
+npm run biome:format # Format code (120 char lines)
+npm run biome:ai # Machine-readable JSON output for AI
+npm run biome:ai-fix # Auto-fix with JSON output
+
+# Testing
+npm run test # Run all tests in watch mode
+npm run test:ui # Run with Vitest UI interface
+npm run test:coverage:stream # Run once with streaming output
+vitest run src/features/projects # Test specific directory
+
+# TypeScript
+npx tsc --noEmit # Check all TypeScript errors
+npx tsc --noEmit 2>&1 | grep "src/features" # Check features only
```
### Backend (python/)
```bash
-# Using uv package manager
-uv sync # Install/update dependencies
-uv run pytest # Run tests
-uv run python -m src.server.main # Run server locally
-
-# With Docker
-docker-compose up --build -d # Start all services
-docker-compose logs -f # View logs
-docker-compose restart # Restart services
+# Using uv package manager (preferred)
+uv sync --group all # Install all dependencies
+uv run python -m src.server.main # Run server locally on 8181
+uv run pytest # Run all tests
+uv run pytest tests/test_api_essentials.py -v # Run specific test
+uv run ruff check # Run linter
+uv run ruff check --fix # Auto-fix linting issues
+uv run mypy src/ # Type check
+
+# Docker operations
+docker compose up --build -d # Start all services
+docker compose --profile backend up -d # Backend only (for hybrid dev)
+docker compose logs -f archon-server # View server logs
+docker compose logs -f archon-mcp # View MCP server logs
+docker compose restart archon-server # Restart after code changes
+docker compose down # Stop all services
+docker compose down -v # Stop and remove volumes
```
-### Testing
+### Quick Workflows
```bash
-# Frontend tests (from archon-ui-main/)
-npm run test:coverage:stream # Run with streaming output
-npm run test:ui # Run with Vitest UI
+# Hybrid development (recommended) - backend in Docker, frontend local
+make dev # Or manually: docker compose --profile backend up -d && cd archon-ui-main && npm run dev
+
+# Full Docker mode
+make dev-docker # Or: docker compose up --build -d
+
+# Run linters before committing
+make lint # Runs both frontend and backend linters
+make lint-fe # Frontend only (ESLint + Biome)
+make lint-be # Backend only (Ruff + MyPy)
-# Backend tests (from python/)
-uv run pytest tests/test_api_essentials.py -v
-uv run pytest tests/test_service_integration.py -v
+# Testing
+make test # Run all tests
+make test-fe # Frontend tests only
+make test-be # Backend tests only
```
-## Key API Endpoints
+## Architecture Overview
-### Knowledge Base
+Archon V2 Alpha is a microservices-based knowledge management system with MCP (Model Context Protocol) integration:
-- `POST /api/knowledge/crawl` - Crawl a website
-- `POST /api/knowledge/upload` - Upload documents (PDF, DOCX, MD)
-- `GET /api/knowledge/items` - List knowledge items
-- `POST /api/knowledge/search` - RAG search
+### Service Architecture
-### MCP Integration
+- **Frontend (port 3737)**: React + TypeScript + Vite + TailwindCSS
+ - **Dual UI Strategy**:
+ - `/features` - Modern vertical slice with Radix UI primitives + TanStack Query
+ - `/components` - Legacy custom components (being migrated)
+ - **State Management**: TanStack Query for all data fetching (no prop drilling)
+ - **Styling**: Tron-inspired glassmorphism with Tailwind CSS
+ - **Linting**: Biome for `/features`, ESLint for legacy code
-- `GET /api/mcp/health` - MCP server status
-- `POST /api/mcp/tools/{tool_name}` - Execute MCP tool
-- `GET /api/mcp/tools` - List available tools
+- **Main Server (port 8181)**: FastAPI with HTTP polling for updates
+ - Handles all business logic, database operations, and external API calls
+ - WebSocket support removed in favor of HTTP polling with ETag caching
-### Projects & Tasks (when enabled)
+- **MCP Server (port 8051)**: Lightweight HTTP-based MCP protocol server
+ - Provides tools for AI assistants (Claude, Cursor, Windsurf)
+ - Exposes knowledge search, task management, and project operations
-- `GET /api/projects` - List all projects
-- `POST /api/projects` - Create project
-- `GET /api/projects/{id}` - Get single project
-- `PUT /api/projects/{id}` - Update project
-- `DELETE /api/projects/{id}` - Delete project
-- `GET /api/projects/{id}/tasks` - Get tasks for project (use this, not getTasks)
-- `POST /api/tasks` - Create task
-- `PUT /api/tasks/{id}` - Update task
-- `DELETE /api/tasks/{id}` - Delete task
+- **Agents Service (port 8052)**: PydanticAI agents for AI/ML operations
+ - Handles complex AI workflows and document processing
-## Polling Architecture
+- **Database**: Supabase (PostgreSQL + pgvector for embeddings)
+ - Cloud or local Supabase both supported
+ - pgvector for semantic search capabilities
-### HTTP Polling (replaced Socket.IO)
-- **Polling intervals**: 1-2s for active operations, 5-10s for background data
-- **ETag caching**: Reduces bandwidth by ~70% via 304 Not Modified responses
-- **Smart pausing**: Stops polling when browser tab is inactive
-- **Progress endpoints**: `/api/progress/crawl`, `/api/progress/project-creation`
+### Frontend Architecture Details
-### Key Polling Hooks
-- `usePolling` - Generic polling with ETag support
-- `useDatabaseMutation` - Optimistic updates with rollback
-- `useProjectMutation` - Project-specific operations
+#### Vertical Slice Architecture (/features)
-## Environment Variables
+Features are organized by domain hierarchy with self-contained modules:
-Required in `.env`:
+```
+src/features/
+├── ui/
+│ ├── primitives/ # Radix UI base components
+│ ├── hooks/ # Shared UI hooks (useSmartPolling, etc)
+│ └── types/ # UI type definitions
+├── projects/
+│ ├── components/ # Project UI components
+│ ├── hooks/ # Project hooks (useProjectQueries, etc)
+│ ├── services/ # Project API services
+│ ├── types/ # Project type definitions
+│ ├── tasks/ # Tasks sub-feature (nested under projects)
+│ │ ├── components/
+│ │ ├── hooks/ # Task-specific hooks
+│ │ ├── services/ # Task API services
+│ │ └── types/
+│ └── documents/ # Documents sub-feature
+│ ├── components/
+│ ├── services/
+│ └── types/
+```
-```bash
-SUPABASE_URL=https://your-project.supabase.co
-SUPABASE_SERVICE_KEY=your-service-key-here
+#### TanStack Query Patterns
+
+All data fetching uses TanStack Query with consistent patterns:
+
+```typescript
+// Query keys factory pattern
+export const projectKeys = {
+ all: ["projects"] as const,
+ lists: () => [...projectKeys.all, "list"] as const,
+ detail: (id: string) => [...projectKeys.all, "detail", id] as const,
+};
+
+// Smart polling with visibility awareness
+const { refetchInterval } = useSmartPolling(10000); // Pauses when tab inactive
+
+// Optimistic updates with rollback
+useMutation({
+ onMutate: async (data) => {
+ await queryClient.cancelQueries(key);
+ const previous = queryClient.getQueryData(key);
+ queryClient.setQueryData(key, optimisticData);
+ return { previous };
+ },
+ onError: (err, vars, context) => {
+ if (context?.previous) {
+ queryClient.setQueryData(key, context.previous);
+ }
+ },
+});
```
-Optional:
+### Backend Architecture Details
-```bash
-OPENAI_API_KEY=your-openai-key # Can be set via UI
-LOGFIRE_TOKEN=your-logfire-token # For observability
-LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
+#### Service Layer Pattern
+
+```python
+# API Route -> Service -> Database
+# src/server/api_routes/projects.py
+@router.get("/{project_id}")
+async def get_project(project_id: str):
+ return await project_service.get_project(project_id)
+
+# src/server/services/project_service.py
+async def get_project(project_id: str):
+ # Business logic here
+ return await db.fetch_project(project_id)
```
-## File Organization
+#### Error Handling Patterns
-### Frontend Structure
+```python
+# Use specific exceptions
+class ProjectNotFoundError(Exception): pass
+class ValidationError(Exception): pass
+
+# Rich error responses
+@app.exception_handler(ProjectNotFoundError)
+async def handle_not_found(request, exc):
+ return JSONResponse(
+ status_code=404,
+ content={"detail": str(exc), "type": "not_found"}
+ )
+```
-- `src/components/` - Reusable UI components
-- `src/pages/` - Main application pages
-- `src/services/` - API communication and business logic
-- `src/hooks/` - Custom React hooks
-- `src/contexts/` - React context providers
+## Polling Architecture
+
+### HTTP Polling (replaced Socket.IO)
+
+- **Polling intervals**: 1-2s for active operations, 5-10s for background data
+- **ETag caching**: Reduces bandwidth by ~70% via 304 Not Modified responses
+- **Smart pausing**: Stops polling when browser tab is inactive
+- **Progress endpoints**: `/api/progress/{id}` for operation tracking
-### Backend Structure
+### Key Polling Hooks
-- `src/server/` - Main FastAPI application
-- `src/server/api_routes/` - API route handlers
-- `src/server/services/` - Business logic services
-- `src/mcp/` - MCP server implementation
-- `src/agents/` - PydanticAI agent implementations
+- `useSmartPolling` - Adjusts interval based on page visibility/focus
+- `useCrawlProgressPolling` - Specialized for crawl progress with auto-cleanup
+- `useProjectTasks` - Smart polling for task lists
## Database Schema
Key tables in Supabase:
- `sources` - Crawled websites and uploaded documents
+ - Stores metadata, crawl status, and configuration
- `documents` - Processed document chunks with embeddings
+ - Text chunks with vector embeddings for semantic search
- `projects` - Project management (optional feature)
+ - Contains features array, documents, and metadata
- `tasks` - Task tracking linked to projects
+ - Status: todo, doing, review, done
+ - Assignee: User, Archon, AI IDE Agent
- `code_examples` - Extracted code snippets
+ - Language, summary, and relevance metadata
## API Naming Conventions
### Task Status Values
+
Use database values directly (no UI mapping):
+
- `todo`, `doing`, `review`, `done`
### Service Method Patterns
+
- `get[Resource]sByProject(projectId)` - Scoped queries
- `get[Resource](id)` - Single resource
- `create[Resource](data)` - Create operations
@@ -249,10 +341,30 @@ Use database values directly (no UI mapping):
- `delete[Resource](id)` - Soft deletes
### State Naming
+
- `is[Action]ing` - Loading states (e.g., `isSwitchingProject`)
- `[resource]Error` - Error messages
- `selected[Resource]` - Current selection
+## Environment Variables
+
+Required in `.env`:
+
+```bash
+SUPABASE_URL=https://your-project.supabase.co # Or http://host.docker.internal:8000 for local
+SUPABASE_SERVICE_KEY=your-service-key-here # Use legacy key format for cloud Supabase
+```
+
+Optional:
+
+```bash
+LOGFIRE_TOKEN=your-logfire-token # For observability
+LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
+ARCHON_SERVER_PORT=8181 # Server port
+ARCHON_MCP_PORT=8051 # MCP server port
+ARCHON_UI_PORT=3737 # Frontend port
+```
+
## Common Development Tasks
### Add a new API endpoint
@@ -260,47 +372,72 @@ Use database values directly (no UI mapping):
1. Create route handler in `python/src/server/api_routes/`
2. Add service logic in `python/src/server/services/`
3. Include router in `python/src/server/main.py`
-4. Update frontend service in `archon-ui-main/src/services/`
+4. Update frontend service in `archon-ui-main/src/features/[feature]/services/`
-### Add a new UI component
+### Add a new UI component in features directory
-1. Create component in `archon-ui-main/src/components/`
-2. Add to page in `archon-ui-main/src/pages/`
-3. Include any new API calls in services
-4. Add tests in `archon-ui-main/test/`
+1. Use Radix UI primitives from `src/features/ui/primitives/`
+2. Create component in relevant feature folder under `src/features/[feature]/components/`
+3. Define types in `src/features/[feature]/types/`
+4. Use TanStack Query hook from `src/features/[feature]/hooks/`
+5. Apply Tron-inspired glassmorphism styling with Tailwind
### Debug MCP connection issues
1. Check MCP health: `curl http://localhost:8051/health`
-2. View MCP logs: `docker-compose logs archon-mcp`
+2. View MCP logs: `docker compose logs archon-mcp`
3. Test tool execution via UI MCP page
4. Verify Supabase connection and credentials
+### Fix TypeScript/Linting Issues
+
+```bash
+# TypeScript errors in features
+npx tsc --noEmit 2>&1 | grep "src/features"
+
+# Biome auto-fix for features
+npm run biome:fix
+
+# ESLint for legacy code
+npm run lint:files src/components/SomeComponent.tsx
+```
+
## Code Quality Standards
-We enforce code quality through automated linting and type checking:
+### Frontend
+
+- **TypeScript**: Strict mode enabled, no implicit any
+- **Biome** for `/src/features/`: 120 char lines, double quotes, trailing commas
+- **ESLint** for legacy code: Standard React rules
+- **Testing**: Vitest with React Testing Library
+
+### Backend
- **Python 3.12** with 120 character line length
-- **Ruff** for linting - checks for errors, warnings, unused imports, and code style
-- **Mypy** for type checking - ensures type safety across the codebase
-- **Auto-formatting** on save in IDEs to maintain consistent style
-- Run `uv run ruff check` and `uv run mypy src/` locally before committing
+- **Ruff** for linting - checks for errors, warnings, unused imports
+- **Mypy** for type checking - ensures type safety
+- **Pytest** for testing with async support
## MCP Tools Available
-When connected to Cursor/Windsurf:
+When connected to Client/Cursor/Windsurf:
- `archon:perform_rag_query` - Search knowledge base
- `archon:search_code_examples` - Find code snippets
-- `archon:manage_project` - Project operations
-- `archon:manage_task` - Task management
+- `archon:create_project` - Create new project
+- `archon:list_projects` - List all projects
+- `archon:create_task` - Create task in project
+- `archon:list_tasks` - List and filter tasks
+- `archon:update_task` - Update task status/details
- `archon:get_available_sources` - List knowledge sources
## Important Notes
- Projects feature is optional - toggle in Settings UI
- All services communicate via HTTP, not gRPC
-- HTTP polling handles all updates (Socket.IO removed)
+- HTTP polling handles all updates
- Frontend uses Vite proxy for API calls in development
- Python backend uses `uv` for dependency management
- Docker Compose handles service orchestration
+- TanStack Query for all data fetching - NO PROP DRILLING
+- Vertical slice architecture in `/features` - features own their sub-features
diff --git a/archon-ui-main/.dockerignore b/archon-ui-main/.dockerignore
index bbae03653d..9e1e281898 100644
--- a/archon-ui-main/.dockerignore
+++ b/archon-ui-main/.dockerignore
@@ -43,6 +43,16 @@ docker-compose.yml
# Tests
coverage
test-results
+tests/
+**/*.test.ts
+**/*.test.tsx
+**/*.spec.ts
+**/*.spec.tsx
+**/__tests__
+**/*.e2e.test.ts
+**/*.integration.test.ts
+vitest.config.ts
+tsconfig.prod.json
# Documentation
README.md
diff --git a/archon-ui-main/.eslintrc.cjs b/archon-ui-main/.eslintrc.cjs
index f7de173a9a..62100daacf 100644
--- a/archon-ui-main/.eslintrc.cjs
+++ b/archon-ui-main/.eslintrc.cjs
@@ -6,28 +6,119 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
- ignorePatterns: ['dist', '.eslintrc.cjs'],
+ ignorePatterns: [
+ 'dist',
+ '.eslintrc.cjs',
+ 'public',
+ '__mocks__',
+ '*.config.js',
+ '*.config.ts',
+ 'coverage',
+ 'node_modules',
+ 'src/features/**' // Biome handles this directory
+ ],
parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
plugins: ['react-refresh'],
rules: {
+ /**
+ * LINTING STRATEGY FOR ALPHA DEVELOPMENT:
+ *
+ * Development: Warnings don't block local development, allowing rapid iteration
+ * CI/PR: Run with --max-warnings 0 to treat warnings as errors before merge
+ *
+ * Philosophy:
+ * - Strict typing where it helps AI assistants (Claude Code, Copilot, etc.)
+ * - Pragmatic flexibility for alpha-stage rapid development
+ * - Console.log allowed locally but caught in CI
+ * - Progressive enhancement: stricter rules in /features (new code) vs /components (legacy)
+ */
+
+ // React Refresh
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
- '@typescript-eslint/no-unused-vars': ['warn', {
+
+ // TypeScript - Pragmatic strictness for AI-assisted development
+ '@typescript-eslint/no-explicit-any': 'warn', // Visible but won't block development
+ '@typescript-eslint/no-non-null-assertion': 'warn', // Allow when developer is certain
+ '@typescript-eslint/no-empty-function': 'warn', // Sometimes needed for placeholders
+ '@typescript-eslint/ban-types': 'error', // Keep strict - prevents real issues
+
+ // Help AI assistants understand code intent
+ '@typescript-eslint/explicit-function-return-type': ['warn', {
+ allowExpressions: true,
+ allowTypedFunctionExpressions: true,
+ allowHigherOrderFunctions: true,
+ allowDirectConstAssertionInArrowFunctions: true,
+ }],
+
+ // Better TypeScript patterns
+ '@typescript-eslint/prefer-as-const': 'error',
+
+ // Variable and import management - strict with escape hatches
+ '@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
- ignoreRestSiblings: true
+ ignoreRestSiblings: true,
+ destructuredArrayIgnorePattern: '^_'
}],
- '@typescript-eslint/no-explicit-any': 'off',
- '@typescript-eslint/no-empty-function': 'off',
- '@typescript-eslint/ban-types': 'off',
- '@typescript-eslint/no-non-null-assertion': 'warn',
- '@typescript-eslint/no-inferrable-types': 'off',
+
+ // React hooks - warn to allow intentional omissions during development
'react-hooks/exhaustive-deps': 'warn',
- 'no-case-declarations': 'off',
- 'no-constant-condition': 'warn',
- 'prefer-const': 'warn',
- 'no-undef': 'off',
+
+ // Console usage - warn locally, CI treats as error
+ 'no-console': ['warn', { allow: ['error', 'warn'] }], // console.log caught but not blocking
+
+ // General code quality
+ 'prefer-const': 'error',
+ 'no-var': 'error',
+ 'no-constant-condition': 'error',
+ 'no-debugger': 'warn', // Warn in dev, error in CI
+ 'no-alert': 'error',
+
+ // Disable rules that conflict with TypeScript
+ 'no-undef': 'off', // TypeScript handles this better
+ 'no-unused-vars': 'off', // Use @typescript-eslint/no-unused-vars instead
},
-}
+
+ // Override rules for specific file types and directories
+ overrides: [
+ {
+ // Stricter rules for new vertical slice architecture
+ files: ['src/features/**/*.{ts,tsx}'],
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'error', // No any in new code
+ '@typescript-eslint/explicit-function-return-type': ['error', {
+ allowExpressions: true,
+ allowTypedFunctionExpressions: true,
+ }],
+ 'no-console': ['error', { allow: ['error', 'warn'] }], // Stricter console usage
+ }
+ },
+ {
+ // More lenient for legacy components being migrated
+ files: ['src/components/**/*.{ts,tsx}', 'src/services/**/*.{ts,tsx}'],
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'warn', // Still visible during migration
+ '@typescript-eslint/explicit-function-return-type': 'off', // Not required for legacy
+ 'no-console': 'warn', // Warn during migration
+ }
+ },
+ {
+ // Test files - most lenient but still helpful
+ files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', 'test/**/*'],
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'warn', // OK in tests but still visible
+ '@typescript-eslint/no-non-null-assertion': 'off', // Fine in tests
+ '@typescript-eslint/no-empty-function': 'off', // Mock functions need this
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ 'no-console': 'off', // Debugging in tests is fine
+ }
+ }
+ ]
+};
\ No newline at end of file
diff --git a/archon-ui-main/__mocks__/lucide-react.tsx b/archon-ui-main/__mocks__/lucide-react.tsx
deleted file mode 100644
index a3553fe118..0000000000
--- a/archon-ui-main/__mocks__/lucide-react.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-import React from 'react'
-import { vi } from 'vitest'
-
-const createMockIcon = (name: string) => {
- const MockIcon = React.forwardRef(({ className, ...props }: any, ref: any) => (
-
- {name}
-
- ))
- MockIcon.displayName = name
- return MockIcon
-}
-
-// Export all icons used in the app
-export const Settings = createMockIcon('Settings')
-export const Check = createMockIcon('Check')
-export const CheckCircle = createMockIcon('CheckCircle')
-export const X = createMockIcon('X')
-export const XCircle = createMockIcon('XCircle')
-export const Eye = createMockIcon('Eye')
-export const EyeOff = createMockIcon('EyeOff')
-export const Save = createMockIcon('Save')
-export const Loader = createMockIcon('Loader')
-export const Loader2 = createMockIcon('Loader2')
-export const RefreshCw = createMockIcon('RefreshCw')
-export const Play = createMockIcon('Play')
-export const Pause = createMockIcon('Pause')
-export const Square = createMockIcon('Square')
-export const FileText = createMockIcon('FileText')
-export const Download = createMockIcon('Download')
-export const Upload = createMockIcon('Upload')
-export const ChevronDown = createMockIcon('ChevronDown')
-export const ChevronUp = createMockIcon('ChevronUp')
-export const ChevronLeft = createMockIcon('ChevronLeft')
-export const ChevronRight = createMockIcon('ChevronRight')
-export const Plus = createMockIcon('Plus')
-export const Minus = createMockIcon('Minus')
-export const Edit = createMockIcon('Edit')
-export const Edit2 = createMockIcon('Edit2')
-export const Edit3 = createMockIcon('Edit3')
-export const Trash = createMockIcon('Trash')
-export const Trash2 = createMockIcon('Trash2')
-export const User = createMockIcon('User')
-export const Users = createMockIcon('Users')
-export const Bot = createMockIcon('Bot')
-export const Database = createMockIcon('Database')
-export const Server = createMockIcon('Server')
-export const Globe = createMockIcon('Globe')
-export const Search = createMockIcon('Search')
-export const Filter = createMockIcon('Filter')
-export const Copy = createMockIcon('Copy')
-export const ExternalLink = createMockIcon('ExternalLink')
-export const Info = createMockIcon('Info')
-export const AlertCircle = createMockIcon('AlertCircle')
-export const AlertTriangle = createMockIcon('AlertTriangle')
-export const Zap = createMockIcon('Zap')
-export const Code = createMockIcon('Code')
-export const Terminal = createMockIcon('Terminal')
-export const Book = createMockIcon('Book')
-export const BookOpen = createMockIcon('BookOpen')
-export const Folder = createMockIcon('Folder')
-export const FolderOpen = createMockIcon('FolderOpen')
-export const File = createMockIcon('File')
-export const Hash = createMockIcon('Hash')
-export const Tag = createMockIcon('Tag')
-export const Clock = createMockIcon('Clock')
-export const Calendar = createMockIcon('Calendar')
-export const MapPin = createMockIcon('MapPin')
-export const Link = createMockIcon('Link')
-export const Mail = createMockIcon('Mail')
-export const Phone = createMockIcon('Phone')
-export const Home = createMockIcon('Home')
-export const Menu = createMockIcon('Menu')
-export const MoreHorizontal = createMockIcon('MoreHorizontal')
-export const MoreVertical = createMockIcon('MoreVertical')
-export const Refresh = createMockIcon('Refresh')
-export const RotateCcw = createMockIcon('RotateCcw')
-export const RotateCw = createMockIcon('RotateCw')
-export const Sun = createMockIcon('Sun')
-export const Moon = createMockIcon('Moon')
-export const Monitor = createMockIcon('Monitor')
-export const Wifi = createMockIcon('Wifi')
-export const WifiOff = createMockIcon('WifiOff')
-export const Volume2 = createMockIcon('Volume2')
-export const VolumeX = createMockIcon('VolumeX')
-export const BarChart = createMockIcon('BarChart')
-export const PieChart = createMockIcon('PieChart')
-export const TrendingUp = createMockIcon('TrendingUp')
-export const TrendingDown = createMockIcon('TrendingDown')
-export const ArrowUp = createMockIcon('ArrowUp')
-export const ArrowDown = createMockIcon('ArrowDown')
-export const ArrowLeft = createMockIcon('ArrowLeft')
-export const ArrowRight = createMockIcon('ArrowRight')
-export const Send = createMockIcon('Send')
-export const MessageSquare = createMockIcon('MessageSquare')
-export const MessageCircle = createMockIcon('MessageCircle')
-export const Heart = createMockIcon('Heart')
-export const Star = createMockIcon('Star')
-export const Bookmark = createMockIcon('Bookmark')
-export const Share = createMockIcon('Share')
-export const Share2 = createMockIcon('Share2')
-export const Maximize = createMockIcon('Maximize')
-export const Minimize = createMockIcon('Minimize')
-export const Expand = createMockIcon('Expand')
-export const Shrink = createMockIcon('Shrink')
-export const Move = createMockIcon('Move')
-export const Shuffle = createMockIcon('Shuffle')
-export const Repeat = createMockIcon('Repeat')
-export const StopCircle = createMockIcon('StopCircle')
-export const SkipBack = createMockIcon('SkipBack')
-export const SkipForward = createMockIcon('SkipForward')
-export const FastForward = createMockIcon('FastForward')
-export const Rewind = createMockIcon('Rewind')
-export const Camera = createMockIcon('Camera')
-export const Image = createMockIcon('Image')
-export const Video = createMockIcon('Video')
-export const Mic = createMockIcon('Mic')
-export const MicOff = createMockIcon('MicOff')
-export const Headphones = createMockIcon('Headphones')
-export const Speaker = createMockIcon('Speaker')
-export const Bell = createMockIcon('Bell')
-export const BellOff = createMockIcon('BellOff')
-export const Shield = createMockIcon('Shield')
-export const ShieldCheck = createMockIcon('ShieldCheck')
-export const ShieldAlert = createMockIcon('ShieldAlert')
-export const Key = createMockIcon('Key')
-export const Lock = createMockIcon('Lock')
-export const Unlock = createMockIcon('Unlock')
-export const LogIn = createMockIcon('LogIn')
-export const LogOut = createMockIcon('LogOut')
-export const UserPlus = createMockIcon('UserPlus')
-export const UserMinus = createMockIcon('UserMinus')
-export const UserCheck = createMockIcon('UserCheck')
-export const UserX = createMockIcon('UserX')
-export const Package = createMockIcon('Package')
-export const Package2 = createMockIcon('Package2')
-export const ShoppingCart = createMockIcon('ShoppingCart')
-export const ShoppingBag = createMockIcon('ShoppingBag')
-export const CreditCard = createMockIcon('CreditCard')
-export const DollarSign = createMockIcon('DollarSign')
-export const Percent = createMockIcon('Percent')
-export const Activity = createMockIcon('Activity')
-export const Cpu = createMockIcon('Cpu')
-export const HardDrive = createMockIcon('HardDrive')
-export const MemoryStick = createMockIcon('MemoryStick')
-export const Smartphone = createMockIcon('Smartphone')
-export const Tablet = createMockIcon('Tablet')
-export const Laptop = createMockIcon('Laptop')
-export const Monitor2 = createMockIcon('Monitor2')
-export const Tv = createMockIcon('Tv')
-export const Watch = createMockIcon('Watch')
-export const Gamepad2 = createMockIcon('Gamepad2')
-export const Mouse = createMockIcon('Mouse')
-export const Keyboard = createMockIcon('Keyboard')
-export const Printer = createMockIcon('Printer')
-export const Scanner = createMockIcon('Scanner')
-export const Webcam = createMockIcon('Webcam')
-export const Bluetooth = createMockIcon('Bluetooth')
-export const Usb = createMockIcon('Usb')
-export const Zap2 = createMockIcon('Zap2')
-export const Battery = createMockIcon('Battery')
-export const BatteryCharging = createMockIcon('BatteryCharging')
-export const Plug = createMockIcon('Plug')
-export const Power = createMockIcon('Power')
-export const PowerOff = createMockIcon('PowerOff')
-export const BarChart2 = createMockIcon('BarChart2')
-export const BarChart3 = createMockIcon('BarChart3')
-export const BarChart4 = createMockIcon('BarChart4')
-export const LineChart = createMockIcon('LineChart')
-export const PieChart2 = createMockIcon('PieChart2')
-export const Layers = createMockIcon('Layers')
-export const Layers2 = createMockIcon('Layers2')
-export const Layers3 = createMockIcon('Layers3')
-export const Grid = createMockIcon('Grid')
-export const Grid2x2 = createMockIcon('Grid2x2')
-export const Grid3x3 = createMockIcon('Grid3x3')
-export const List = createMockIcon('List')
-export const ListChecks = createMockIcon('ListChecks')
-export const ListTodo = createMockIcon('ListTodo')
-export const CheckSquare = createMockIcon('CheckSquare')
-export const Square2 = createMockIcon('Square2')
-export const Circle = createMockIcon('Circle')
-export const CircleCheck = createMockIcon('CircleCheck')
-export const CircleX = createMockIcon('CircleX')
-export const CircleDot = createMockIcon('CircleDot')
-export const Target = createMockIcon('Target')
-export const Focus = createMockIcon('Focus')
-export const Crosshair = createMockIcon('Crosshair')
-export const Locate = createMockIcon('Locate')
-export const LocateFixed = createMockIcon('LocateFixed')
-export const Navigation = createMockIcon('Navigation')
-export const Navigation2 = createMockIcon('Navigation2')
-export const Compass = createMockIcon('Compass')
-export const Map = createMockIcon('Map')
-export const TestTube = createMockIcon('TestTube')
-export const FlaskConical = createMockIcon('FlaskConical')
-export const Bug = createMockIcon('Bug')
-export const GitBranch = createMockIcon('GitBranch')
-export const GitCommit = createMockIcon('GitCommit')
-export const GitMerge = createMockIcon('GitMerge')
-export const GitPullRequest = createMockIcon('GitPullRequest')
-export const Github = createMockIcon('Github')
-export const Gitlab = createMockIcon('Gitlab')
-export const Bitbucket = createMockIcon('Bitbucket')
-export const Network = createMockIcon('Network')
-export const GitGraph = createMockIcon('GitGraph')
-export const ListFilter = createMockIcon('ListFilter')
-export const CheckSquare2 = createMockIcon('CheckSquare2')
-export const CircleSlash2 = createMockIcon('CircleSlash2')
-export const Clock3 = createMockIcon('Clock3')
-export const GitCommitHorizontal = createMockIcon('GitCommitHorizontal')
-export const CalendarDays = createMockIcon('CalendarDays')
-export const Sparkles = createMockIcon('Sparkles')
-export const Layout = createMockIcon('Layout')
-export const Table = createMockIcon('Table')
-export const Columns = createMockIcon('Columns')
-export const GitPullRequestDraft = createMockIcon('GitPullRequestDraft')
-export const BrainCircuit = createMockIcon('BrainCircuit')
-export const Wrench = createMockIcon('Wrench')
-export const PlugZap = createMockIcon('PlugZap')
-export const BoxIcon = createMockIcon('BoxIcon')
-export const Box = createMockIcon('Box')
-export const Brain = createMockIcon('Brain')
-export const LinkIcon = createMockIcon('LinkIcon')
-export const Sparkle = createMockIcon('Sparkle')
-export const FolderTree = createMockIcon('FolderTree')
-export const Lightbulb = createMockIcon('Lightbulb')
-export const Rocket = createMockIcon('Rocket')
-export const Building = createMockIcon('Building')
-export const FileCode = createMockIcon('FileCode')
-export const FileJson = createMockIcon('FileJson')
-export const Braces = createMockIcon('Braces')
-export const Binary = createMockIcon('Binary')
-export const Palette = createMockIcon('Palette')
-export const Paintbrush = createMockIcon('Paintbrush')
-export const Type = createMockIcon('Type')
-export const Heading = createMockIcon('Heading')
-export const AlignLeft = createMockIcon('AlignLeft')
-export const AlignCenter = createMockIcon('AlignCenter')
-export const AlignRight = createMockIcon('AlignRight')
-export const Bold = createMockIcon('Bold')
-export const Italic = createMockIcon('Italic')
-export const Underline = createMockIcon('Underline')
-export const Strikethrough = createMockIcon('Strikethrough')
-export const FileCheck = createMockIcon('FileCheck')
-export const FileX = createMockIcon('FileX')
-export const FilePlus = createMockIcon('FilePlus')
-export const FileMinus = createMockIcon('FileMinus')
-export const FolderPlus = createMockIcon('FolderPlus')
-export const FolderMinus = createMockIcon('FolderMinus')
-export const FolderCheck = createMockIcon('FolderCheck')
-export const FolderX = createMockIcon('FolderX')
-export const startMCPServer = createMockIcon('startMCPServer')
-export const Pin = createMockIcon('Pin')
-export const CheckCircle2 = createMockIcon('CheckCircle2')
-export const Clipboard = createMockIcon('Clipboard')
-export const LayoutGrid = createMockIcon('LayoutGrid')
-export const Pencil = createMockIcon('Pencil')
-export const MousePointer = createMockIcon('MousePointer')
-export const GripVertical = createMockIcon('GripVertical')
-export const History = createMockIcon('History')
-export const PlusCircle = createMockIcon('PlusCircle')
-export const MinusCircle = createMockIcon('MinusCircle')
-export const ChevronDownIcon = createMockIcon('ChevronDownIcon')
-export const FileIcon = createMockIcon('FileIcon')
-export const AlertCircleIcon = createMockIcon('AlertCircleIcon')
-export const Clock4 = createMockIcon('Clock4')
-export const XIcon = createMockIcon('XIcon')
-export const CheckIcon = createMockIcon('CheckIcon')
-export const TrashIcon = createMockIcon('TrashIcon')
-export const EyeIcon = createMockIcon('EyeIcon')
-export const EditIcon = createMockIcon('EditIcon')
-export const DownloadIcon = createMockIcon('DownloadIcon')
-export const RefreshIcon = createMockIcon('RefreshIcon')
-export const SearchIcon = createMockIcon('SearchIcon')
-export const FilterIcon = createMockIcon('FilterIcon')
-export const PlusIcon = createMockIcon('PlusIcon')
-export const FolderIcon = createMockIcon('FolderIcon')
-export const FileTextIcon = createMockIcon('FileTextIcon')
-export const BookOpenIcon = createMockIcon('BookOpenIcon')
-export const DatabaseIcon = createMockIcon('DatabaseIcon')
-export const GlobeIcon = createMockIcon('GlobeIcon')
-export const TagIcon = createMockIcon('TagIcon')
-export const CalendarIcon = createMockIcon('CalendarIcon')
-export const ClockIcon = createMockIcon('ClockIcon')
-export const UserIcon = createMockIcon('UserIcon')
-export const SettingsIcon = createMockIcon('SettingsIcon')
-export const InfoIcon = createMockIcon('InfoIcon')
-export const WarningIcon = createMockIcon('WarningIcon')
-export const ErrorIcon = createMockIcon('ErrorIcon')
\ No newline at end of file
diff --git a/archon-ui-main/biome.json b/archon-ui-main/biome.json
new file mode 100644
index 0000000000..2461476d86
--- /dev/null
+++ b/archon-ui-main/biome.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
+ "files": {
+ "includes": ["src/features/**", "src/components/layout/**"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 120,
+ "bracketSpacing": true,
+ "attributePosition": "auto"
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double",
+ "jsxQuoteStyle": "double",
+ "quoteProperties": "asNeeded",
+ "trailingCommas": "all",
+ "semicolons": "always",
+ "arrowParentheses": "always",
+ "bracketSameLine": false
+ }
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": {
+ "level": "on"
+ }
+ }
+ }
+ }
+}
diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json
index c5a0773e96..cb8e8f3687 100644
--- a/archon-ui-main/package-lock.json
+++ b/archon-ui-main/package-lock.json
@@ -8,11 +8,17 @@
"name": "archon-ui",
"version": "0.1.0",
"dependencies": {
- "@milkdown/crepe": "^7.5.0",
- "@milkdown/kit": "^7.5.0",
- "@milkdown/plugin-history": "^7.5.0",
- "@milkdown/preset-commonmark": "^7.5.0",
- "@xyflow/react": "^12.3.0",
+ "@mdxeditor/editor": "^3.42.0",
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toast": "^1.2.15",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-query": "^5.85.8",
+ "@tanstack/react-query-devtools": "^5.85.8",
"clsx": "latest",
"date-fns": "^4.1.0",
"fractional-indexing": "^3.2.0",
@@ -23,24 +29,26 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^6.26.2",
"tailwind-merge": "latest",
"zod": "^3.25.46"
},
"devDependencies": {
+ "@biomejs/biome": "2.2.2",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20.19.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
- "@typescript-eslint/eslint-plugin": "^5.54.0",
- "@typescript-eslint/parser": "^5.54.0",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"autoprefixer": "latest",
- "eslint": "^8.50.0",
+ "eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.1",
"jsdom": "^24.1.0",
@@ -263,6 +271,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -272,6 +281,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -305,6 +315,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.0"
@@ -395,6 +406,7 @@
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -411,6 +423,169 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@biomejs/biome": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.2.tgz",
+ "integrity": "sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "2.2.2",
+ "@biomejs/cli-darwin-x64": "2.2.2",
+ "@biomejs/cli-linux-arm64": "2.2.2",
+ "@biomejs/cli-linux-arm64-musl": "2.2.2",
+ "@biomejs/cli-linux-x64": "2.2.2",
+ "@biomejs/cli-linux-x64-musl": "2.2.2",
+ "@biomejs/cli-win32-arm64": "2.2.2",
+ "@biomejs/cli-win32-x64": "2.2.2"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.2.tgz",
+ "integrity": "sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.2.tgz",
+ "integrity": "sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.2.tgz",
+ "integrity": "sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.2.tgz",
+ "integrity": "sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.2.tgz",
+ "integrity": "sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.2.tgz",
+ "integrity": "sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.2.tgz",
+ "integrity": "sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.2.tgz",
+ "integrity": "sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
@@ -763,6 +938,19 @@
"crelt": "^1.0.5"
}
},
+ "node_modules/@codemirror/merge": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.10.2.tgz",
+ "integrity": "sha512-rmHzVkt5FnCtsi0IgvDIDjh/J4LmbfOboB7FMvVl21IHO0p1QM6jSwjkBjBD3D+c+T79OabEqoduCqvJCBV8Yg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/highlight": "^1.0.0",
+ "style-mod": "^4.1.0"
+ }
+ },
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
@@ -783,18 +971,6 @@
"@marijn/find-cluster-break": "^1.0.0"
}
},
- "node_modules/@codemirror/theme-one-dark": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
- "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
- "license": "MIT",
- "dependencies": {
- "@codemirror/language": "^6.0.0",
- "@codemirror/state": "^6.0.0",
- "@codemirror/view": "^6.0.0",
- "@lezer/highlight": "^1.0.0"
- }
- },
"node_modules/@codemirror/view": {
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
@@ -807,6 +983,61 @@
"w3c-keyname": "^2.2.4"
}
},
+ "node_modules/@codesandbox/nodebox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/@codesandbox/nodebox/-/nodebox-0.1.8.tgz",
+ "integrity": "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg==",
+ "license": "SEE LICENSE IN ./LICENSE",
+ "dependencies": {
+ "outvariant": "^1.4.0",
+ "strict-event-emitter": "^0.4.3"
+ }
+ },
+ "node_modules/@codesandbox/sandpack-client": {
+ "version": "2.19.8",
+ "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-client/-/sandpack-client-2.19.8.tgz",
+ "integrity": "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@codesandbox/nodebox": "0.1.8",
+ "buffer": "^6.0.3",
+ "dequal": "^2.0.2",
+ "mime-db": "^1.52.0",
+ "outvariant": "1.4.0",
+ "static-browser-server": "1.0.3"
+ }
+ },
+ "node_modules/@codesandbox/sandpack-react": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-react/-/sandpack-react-2.20.0.tgz",
+ "integrity": "sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.4.0",
+ "@codemirror/commands": "^6.1.3",
+ "@codemirror/lang-css": "^6.0.1",
+ "@codemirror/lang-html": "^6.4.0",
+ "@codemirror/lang-javascript": "^6.1.2",
+ "@codemirror/language": "^6.3.2",
+ "@codemirror/state": "^6.2.0",
+ "@codemirror/view": "^6.7.1",
+ "@codesandbox/sandpack-client": "^2.19.8",
+ "@lezer/highlight": "^1.1.3",
+ "@react-hook/intersection-observer": "^3.1.1",
+ "@stitches/core": "^1.2.6",
+ "anser": "^2.1.1",
+ "clean-set": "^1.1.2",
+ "dequal": "^2.0.2",
+ "escape-carriage": "^1.3.1",
+ "lz-string": "^1.4.4",
+ "react-devtools-inline": "4.4.0",
+ "react-is": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19",
+ "react-dom": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -1417,28 +1648,56 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
- "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
- "@floating-ui/utils": "^0.2.9"
+ "@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz",
- "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.16",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
+ "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.6",
+ "@floating-ui/utils": "^0.2.10",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
- "@floating-ui/core": "^1.7.1",
- "@floating-ui/utils": "^0.2.9"
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
- "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array": {
@@ -1588,6 +1847,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -1601,6 +1861,261 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@lexical/clipboard": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.33.1.tgz",
+ "integrity": "sha512-Qd3/Cm3TW2DFQv58kMtLi86u5YOgpBdf+o7ySbXz55C613SLACsYQBB3X5Vu5hTx/t/ugYOpII4HkiatW6d9zA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/html": "0.33.1",
+ "@lexical/list": "0.33.1",
+ "@lexical/selection": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/code": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.33.1.tgz",
+ "integrity": "sha512-E0Y/+1znkqVpP52Y6blXGAduoZek9SSehJN+vbH+4iQKyFwTA7JB+jd5C5/K0ik55du9X7SN/oTynByg7lbcAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1",
+ "prismjs": "^1.30.0"
+ }
+ },
+ "node_modules/@lexical/devtools-core": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.33.1.tgz",
+ "integrity": "sha512-3yHu5diNtjwhoe2q/x9as6n6rIfA+QO2CfaVjFRkam8rkAW6zUzQT1D0fQdE8nOfWvXBgY1mH/ZLP4dDXBdG5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/html": "0.33.1",
+ "@lexical/link": "0.33.1",
+ "@lexical/mark": "0.33.1",
+ "@lexical/table": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ },
+ "peerDependencies": {
+ "react": ">=17.x",
+ "react-dom": ">=17.x"
+ }
+ },
+ "node_modules/@lexical/dragon": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.33.1.tgz",
+ "integrity": "sha512-UQ6DLkcDAr83wA1vz3sUgtcpYcMifC4sF0MieZAoMzFrna6Ekqj7OJ7g8Lo7m7AeuT4NETRVDsjIEDdrQMKLLA==",
+ "license": "MIT",
+ "dependencies": {
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/hashtag": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.33.1.tgz",
+ "integrity": "sha512-M3IsDe4cifggMBZgYAVT7hCLWcwQ3dIcUPdr9Xc6wDQQQdEqOQYB0PO//9bSYUVq+BNiiTgysc+TtlM7PiJfiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/history": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.33.1.tgz",
+ "integrity": "sha512-Bk0h3D6cFkJ7w3HKvqQua7n6Xfz7nR7L3gLDBH9L0nsS4MM9+LteSEZPUe0kj4VuEjnxufYstTc9HA2aNLKxnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/html": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.33.1.tgz",
+ "integrity": "sha512-t14vu4eKa6BWz1N7/rwXgXif1k4dj73dRvllWJgfXum+a36vn1aySNYOlOfqWXF7k1b3uJmoqsWK7n/1ASnimw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/selection": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/link": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.33.1.tgz",
+ "integrity": "sha512-JCTu7Fft2J2kgfqJiWnGei+UMIXVKiZKaXzuHCuGQTFu92DeCyd02azBaFazZHEkSqCIFZ0DqVV2SpIJmd0Ygw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/list": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.33.1.tgz",
+ "integrity": "sha512-PXp56dWADSThc9WhwWV4vXhUc3sdtCqsfPD3UQNGUZ9rsAY1479rqYLtfYgEmYPc8JWXikQCAKEejahCJIm8OQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/selection": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/mark": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.33.1.tgz",
+ "integrity": "sha512-tGdOf1e694lnm/HyWUKEkEWjDyfhCBFG7u8iRKNpsYTpB3M1FsJUXbphE2bb8MyWfhHbaNxnklupSSaSPzO88A==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/markdown": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.33.1.tgz",
+ "integrity": "sha512-p5zwWNF70pELRx60wxE8YOFVNiNDkw7gjKoYqkED23q5hj4mcqco9fQf6qeeZChjxLKjfyT6F1PpWgxmlBlxBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/code": "0.33.1",
+ "@lexical/link": "0.33.1",
+ "@lexical/list": "0.33.1",
+ "@lexical/rich-text": "0.33.1",
+ "@lexical/text": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/offset": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.33.1.tgz",
+ "integrity": "sha512-3YIlUs43QdKSBLEfOkuciE2tn9loxVmkSs/HgaIiLYl0Edf1W00FP4ItSmYU4De5GopXsHq6+Y3ry4pU/ciUiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/overflow": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.33.1.tgz",
+ "integrity": "sha512-3BDq1lOw567FeCk4rN2ellKwoXTM9zGkGuKnSGlXS1JmtGGGSvT+uTANX3KOOfqTNSrOkrwoM+3hlFv7p6VpiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/plain-text": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.33.1.tgz",
+ "integrity": "sha512-2HxdhAx6bwF8y5A9P0q3YHsYbhUo4XXm+GyKJO87an8JClL2W+GYLTSDbfNWTh4TtH95eG+UYLOjNEgyU6tsWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/clipboard": "0.33.1",
+ "@lexical/selection": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/react": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.33.1.tgz",
+ "integrity": "sha512-ylnUmom5h8PY+Z14uDmKLQEoikTPN77GRM0NRCIdtbWmOQqOq/5BhuCzMZE1WvpL5C6n3GtK6IFnsMcsKmVOcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.27.8",
+ "@lexical/devtools-core": "0.33.1",
+ "@lexical/dragon": "0.33.1",
+ "@lexical/hashtag": "0.33.1",
+ "@lexical/history": "0.33.1",
+ "@lexical/link": "0.33.1",
+ "@lexical/list": "0.33.1",
+ "@lexical/mark": "0.33.1",
+ "@lexical/markdown": "0.33.1",
+ "@lexical/overflow": "0.33.1",
+ "@lexical/plain-text": "0.33.1",
+ "@lexical/rich-text": "0.33.1",
+ "@lexical/table": "0.33.1",
+ "@lexical/text": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "@lexical/yjs": "0.33.1",
+ "lexical": "0.33.1",
+ "react-error-boundary": "^3.1.4"
+ },
+ "peerDependencies": {
+ "react": ">=17.x",
+ "react-dom": ">=17.x"
+ }
+ },
+ "node_modules/@lexical/rich-text": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.33.1.tgz",
+ "integrity": "sha512-ZBIsj4LwmamRBCGjJiPSLj7N/XkUDv/pnYn5Rp0BL42WpOiQLvOoGLrZxgUJZEmRPQnx42ZgLKVgrWHsyjuoAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/clipboard": "0.33.1",
+ "@lexical/selection": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/selection": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.33.1.tgz",
+ "integrity": "sha512-KXPkdCDdVfIUXmkwePu9DAd3kLjL0aAqL5G9CMCFsj7RG9lLvvKk7kpivrAIbRbcsDzO44QwsFPisZHbX4ioXA==",
+ "license": "MIT",
+ "dependencies": {
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/table": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.33.1.tgz",
+ "integrity": "sha512-pzB11i1Y6fzmy0IPUKJyCdhVBgXaNOxJUxrQJWdKNYCh1eMwwMEQvj+8inItd/11aUkjcdHjwDTht8gL2UHKiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/clipboard": "0.33.1",
+ "@lexical/utils": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/text": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.33.1.tgz",
+ "integrity": "sha512-CnyU3q3RytXXWVSvC5StOKISzFAPGK9MuesNDDGyZk7yDK+J98gV6df4RBKfqwcokFMThpkUlvMeKe1+S2y25A==",
+ "license": "MIT",
+ "dependencies": {
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/utils": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.33.1.tgz",
+ "integrity": "sha512-eKysPjzEE9zD+2af3WRX5U3XbeNk0z4uv1nXGH3RG15uJ4Huzjht82hzsQpCFUobKmzYlQaQs5y2IYKE2puipQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/list": "0.33.1",
+ "@lexical/selection": "0.33.1",
+ "@lexical/table": "0.33.1",
+ "lexical": "0.33.1"
+ }
+ },
+ "node_modules/@lexical/yjs": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.33.1.tgz",
+ "integrity": "sha512-Zx1rabMm/Zjk7n7YQMIQLUN+tqzcg1xqcgNpEHSfK1GA8QMPXCPvXWFT3ZDC4tfZOSy/YIqpVUyWZAomFqRa+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/offset": "0.33.1",
+ "@lexical/selection": "0.33.1",
+ "lexical": "0.33.1"
+ },
+ "peerDependencies": {
+ "yjs": ">=13.5.22"
+ }
+ },
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
@@ -1784,470 +2299,1090 @@
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
- "node_modules/@milkdown/components": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/components/-/components-7.15.1.tgz",
- "integrity": "sha512-yGfjSi7VaRtiyoJA/KGVQtXpig2GYFe7uFAC6kRwskJZF/LQH/+hjjTgpPIcGn+AhKghtJ049ZXnJkgFU45YYA==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.5.1",
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/plugin-tooltip": "7.15.1",
- "@milkdown/preset-commonmark": "7.15.1",
- "@milkdown/preset-gfm": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/transformer": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "@types/lodash.debounce": "^4.0.7",
- "@types/lodash.throttle": "^4.1.9",
- "clsx": "^2.0.0",
- "dompurify": "^3.2.5",
- "lodash.debounce": "^4.0.8",
- "lodash.throttle": "^4.1.1",
- "nanoid": "^5.0.9",
- "tslib": "^2.8.1",
- "unist-util-visit": "^5.0.0",
- "vue": "^3.5.13"
+ "node_modules/@mdxeditor/editor": {
+ "version": "3.42.0",
+ "resolved": "https://registry.npmjs.org/@mdxeditor/editor/-/editor-3.42.0.tgz",
+ "integrity": "sha512-nQN07RkTm842T477IjPqp1FhWCQMpmbLToOVrc6EjSI60aHifwzva+eqYmElHFKE2jyGiD5FsaQXri1SSORJNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/commands": "^6.2.4",
+ "@codemirror/lang-markdown": "^6.2.3",
+ "@codemirror/language-data": "^6.5.1",
+ "@codemirror/merge": "^6.4.0",
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.23.0",
+ "@codesandbox/sandpack-react": "^2.20.0",
+ "@lexical/clipboard": "^0.33.1",
+ "@lexical/link": "^0.33.1",
+ "@lexical/list": "^0.33.1",
+ "@lexical/markdown": "^0.33.1",
+ "@lexical/plain-text": "^0.33.1",
+ "@lexical/react": "^0.33.1",
+ "@lexical/rich-text": "^0.33.1",
+ "@lexical/selection": "^0.33.1",
+ "@lexical/utils": "^0.33.1",
+ "@mdxeditor/gurx": "^1.1.4",
+ "@radix-ui/colors": "^3.0.0",
+ "@radix-ui/react-dialog": "^1.1.11",
+ "@radix-ui/react-icons": "^1.3.2",
+ "@radix-ui/react-popover": "^1.1.11",
+ "@radix-ui/react-popper": "^1.2.4",
+ "@radix-ui/react-select": "^2.2.2",
+ "@radix-ui/react-toggle-group": "^1.1.7",
+ "@radix-ui/react-toolbar": "^1.1.7",
+ "@radix-ui/react-tooltip": "^1.2.4",
+ "classnames": "^2.3.2",
+ "cm6-theme-basic-light": "^0.2.0",
+ "codemirror": "^6.0.1",
+ "downshift": "^7.6.0",
+ "js-yaml": "4.1.0",
+ "lexical": "^0.33.1",
+ "mdast-util-directive": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-frontmatter": "^2.0.1",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-highlight-mark": "^1.2.2",
+ "mdast-util-mdx": "^3.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-to-markdown": "^2.1.0",
+ "micromark-extension-directive": "^3.0.0",
+ "micromark-extension-frontmatter": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.1",
+ "micromark-extension-highlight-mark": "^1.2.0",
+ "micromark-extension-mdx-jsx": "^3.0.0",
+ "micromark-extension-mdx-md": "^2.0.0",
+ "micromark-extension-mdxjs": "^3.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.1",
+ "micromark-util-symbol": "^2.0.0",
+ "react-hook-form": "^7.56.1",
+ "unidiff": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=16"
},
"peerDependencies": {
- "@codemirror/language": "^6",
- "@codemirror/state": "^6",
- "@codemirror/view": "^6"
+ "react": ">= 18 || >= 19",
+ "react-dom": ">= 18 || >= 19"
}
},
- "node_modules/@milkdown/components/node_modules/nanoid": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
- "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/@mdxeditor/gurx": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@mdxeditor/gurx/-/gurx-1.2.3.tgz",
+ "integrity": "sha512-5DQOlEx46oN9spggrC8husAGAhVoEFBGIYKN48es08XhRUbSU6l5bcIQYwRrQaY8clU1tExIcXzw8/fNnoxjpg==",
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.js"
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "react": ">= 18 || >= 19",
+ "react-dom": ">= 18 || >= 19"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
},
"engines": {
- "node": "^18 || >=20"
+ "node": ">= 8"
}
},
- "node_modules/@milkdown/core": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/core/-/core-7.15.1.tgz",
- "integrity": "sha512-jcuKZnZ9rrffwpAFq+0pMIwfxnchZOCFSIQT7NQnsOhzFXnCNXu69cxzlcK3CZExDgkmivHM62xFsjN9l7vTdg==",
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/transformer": "7.15.1",
- "remark-parse": "^11.0.0",
- "remark-stringify": "^11.0.0",
- "tslib": "^2.8.1",
- "unified": "^11.0.3"
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "license": "MIT"
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
}
},
- "node_modules/@milkdown/crepe": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/crepe/-/crepe-7.15.1.tgz",
- "integrity": "sha512-2n63s4vBzTO0IBTO/nIsS/XKyPIpOXjiIwXgkIXkbzNt/sPA6hEwtvEJDCz7uzRo/MCxZu0jLjPW/OpVMakLNg==",
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/colors": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz",
+ "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+ "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
- "@codemirror/commands": "^6.2.4",
- "@codemirror/language": "^6.10.1",
- "@codemirror/language-data": "^6.3.1",
- "@codemirror/state": "^6.4.1",
- "@codemirror/theme-one-dark": "^6.1.2",
- "@codemirror/view": "^6.16.0",
- "@floating-ui/dom": "^1.5.1",
- "@milkdown/kit": "7.15.1",
- "@types/lodash-es": "^4.17.12",
- "clsx": "^2.0.0",
- "codemirror": "^6.0.1",
- "katex": "^0.16.0",
- "lodash-es": "^4.17.21",
- "nanoid": "^5.0.9",
- "prosemirror-virtual-cursor": "^0.4.2",
- "remark-math": "^6.0.0",
- "tslib": "^2.8.1",
- "unist-util-visit": "^5.0.0",
- "vue": "^3.5.13"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/crepe/node_modules/nanoid": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
- "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
- ],
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.js"
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
},
- "engines": {
- "node": "^18 || >=20"
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/ctx": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/ctx/-/ctx-7.15.1.tgz",
- "integrity": "sha512-MO2EymuAmcT9TVcbZVr0TriFMAPP1d1p/cWVbyqZXKsxK1sRzNxJCpdPm20LD2e2qJt6pRziIf/ugGww1Tvf7A==",
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
- "@milkdown/exception": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/exception": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/exception/-/exception-7.15.1.tgz",
- "integrity": "sha512-QMpT/8SYM1CIuptHrOKzaelZd4ZU1j9mz3m2EwF4Ql0PNOXoWW50/P7gtr71foyTu3fPyXA9f8/GaTkihD/b/Q==",
+ "node_modules/@radix-ui/react-icons": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+ "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
- "tslib": "^2.8.1"
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/kit": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/kit/-/kit-7.15.1.tgz",
- "integrity": "sha512-wmUt9mN+rfJgCz11c3z2E8ExIKhd2QLdFPCPb8OHamebsf+td5nS0HX2vpvkaumgD4AQA0KCbMs9WmqChG/K7w==",
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
- "@milkdown/components": "7.15.1",
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/plugin-block": "7.15.1",
- "@milkdown/plugin-clipboard": "7.15.1",
- "@milkdown/plugin-cursor": "7.15.1",
- "@milkdown/plugin-history": "7.15.1",
- "@milkdown/plugin-indent": "7.15.1",
- "@milkdown/plugin-listener": "7.15.1",
- "@milkdown/plugin-slash": "7.15.1",
- "@milkdown/plugin-tooltip": "7.15.1",
- "@milkdown/plugin-trailing": "7.15.1",
- "@milkdown/plugin-upload": "7.15.1",
- "@milkdown/preset-commonmark": "7.15.1",
- "@milkdown/preset-gfm": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/transformer": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-block": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-block/-/plugin-block-7.15.1.tgz",
- "integrity": "sha512-ltftyP6brSs5N3q9mJhcauqfuDuGIGm2dsXwpibsRsO8WbCptVpQPjHCNGJp+/Y+bLrRas5DAz+cqFvfDvCDYA==",
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
- "@floating-ui/dom": "^1.5.1",
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "@types/lodash.throttle": "^4.1.9",
- "lodash.throttle": "^4.1.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-clipboard": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-clipboard/-/plugin-clipboard-7.15.1.tgz",
- "integrity": "sha512-mtq+CQhuCR/bVKHep588OsrIHxQAve85VHIPHPaU768c6jkQhGlr82a0bp90hhzTMREWDqxsFrJAxPM+PDtugA==",
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-cursor": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-cursor/-/plugin-cursor-7.15.1.tgz",
- "integrity": "sha512-HZloO+PpoXvdf854aflIA1pq5cmoRHNvaiC3QCeywAz6y0EHFr0NSJRQQZwXIefdbi5l/CP/lkc9dJJotzgEng==",
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-history": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-history/-/plugin-history-7.15.1.tgz",
- "integrity": "sha512-2LkYbZYuix7LUI/sR1NQO5oZOjcT9E6wJhDHcMmeO8XoIO6r0q8STdH7jvITkB/Rr9wNRXfI+V86hvsfB0aMbw==",
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-indent": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-indent/-/plugin-indent-7.15.1.tgz",
- "integrity": "sha512-D3asSTw6Jvyn3TRVOGNNwhslL0OgnU0Fi9G1JOt9nsaqDIuTMQQhapJzr3VCZn1ko9hdlYUKBQnPkAXngNZKjg==",
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-listener": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-listener/-/plugin-listener-7.15.1.tgz",
- "integrity": "sha512-lnpFzAmhJK0+No0R4utWNx31cDunBqkdBGMBbV6571SHgfVIHw/T8z64t8Fo7xNt9OjcgKee877tk6TaL98HiQ==",
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "@types/lodash.debounce": "^4.0.7",
- "lodash.debounce": "^4.0.8",
- "tslib": "^2.8.1"
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-slash": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-slash/-/plugin-slash-7.15.1.tgz",
- "integrity": "sha512-b/wvpr7+hRgNsftu3XbUMHEKOKUSdNN+HXMhTqTIheB/m/Y7zSdPL3kXMQC1ZRwHyDu1oL6lUuMCMCJ8cDiMvg==",
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"license": "MIT",
"dependencies": {
- "@floating-ui/dom": "^1.5.1",
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "@types/lodash.debounce": "^4.0.7",
- "lodash.debounce": "^4.0.8",
- "tslib": "^2.8.1"
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
+ "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz",
+ "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-toggle-group": "1.1.11"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-tooltip": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-tooltip/-/plugin-tooltip-7.15.1.tgz",
- "integrity": "sha512-Q/TwzqM4CRSTmz0+E/amtNTgk7DJpAOjjCR4am02N2HbYP7GcL92mC1pEx//tus1AW9+LOdF+cmWyObHGkC7Vg==",
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.5.1",
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "@types/lodash.throttle": "^4.1.9",
- "lodash.throttle": "^4.1.1",
- "tslib": "^2.8.1"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-trailing": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-trailing/-/plugin-trailing-7.15.1.tgz",
- "integrity": "sha512-zsECiNOMta4bIy+4a+BplmMwWfrhy3SYcm1kH6DjvkpoBG6LtZC4fblEnlW4feHzDwXVLHkbac5urR4rsZxLcA==",
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/plugin-upload": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/plugin-upload/-/plugin-upload-7.15.1.tgz",
- "integrity": "sha512-9sU2GRERc7lhQ8mSANZ3v1531pmVRImJx4Pr73oB7VPRDS8GbnhtuVI6VewoVh3zJkM8MSE4G4L/TfWXfa2UGQ==",
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "tslib": "^2.8.1"
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/preset-commonmark": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/preset-commonmark/-/preset-commonmark-7.15.1.tgz",
- "integrity": "sha512-P1dewR9TGe8VFIE5F+W9g/2QQzf47EZ+Uq4CF5mYAFbjzPHwJDgfN4vA/o43feXFNxu5cm4UQFWzSCtDNZFTWg==",
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/transformer": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "remark-inline-links": "^7.0.0",
- "tslib": "^2.8.1",
- "unist-util-visit": "^5.0.0",
- "unist-util-visit-parents": "^6.0.1"
- }
- },
- "node_modules/@milkdown/preset-gfm": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/preset-gfm/-/preset-gfm-7.15.1.tgz",
- "integrity": "sha512-cmQsx1lwWGi7vv/8Kx92dToWpqKWLDp5OZSWE0eiLCtAV87v+vL/bT6xDrjqmlpFjA5WEM7ah+Ki3EpqLsRfng==",
- "license": "MIT",
- "dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/preset-commonmark": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/transformer": "7.15.1",
- "@milkdown/utils": "7.15.1",
- "prosemirror-safari-ime-span": "^1.0.1",
- "remark-gfm": "^4.0.1",
- "tslib": "^2.8.1"
- }
- },
- "node_modules/@milkdown/prose": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/prose/-/prose-7.15.1.tgz",
- "integrity": "sha512-8xSgiC6qk8j9zDbRZiWcdclr4vIxU6tnzMBg/Kr4pIEOsir0KA+c6kPNFj7T91BeaV9ksCsWOmYfBwd2SptCXQ==",
- "license": "MIT",
- "dependencies": {
- "@milkdown/exception": "7.15.1",
- "prosemirror-changeset": "^2.2.1",
- "prosemirror-commands": "^1.6.2",
- "prosemirror-dropcursor": "^1.8.1",
- "prosemirror-gapcursor": "^1.3.2",
- "prosemirror-history": "^1.4.1",
- "prosemirror-inputrules": "^1.4.0",
- "prosemirror-keymap": "^1.2.2",
- "prosemirror-model": "^1.24.1",
- "prosemirror-schema-list": "^1.5.0",
- "prosemirror-state": "^1.4.3",
- "prosemirror-tables": "^1.7.0",
- "prosemirror-transform": "^1.10.2",
- "prosemirror-view": "^1.37.1",
- "tslib": "^2.8.1"
- }
- },
- "node_modules/@milkdown/transformer": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/transformer/-/transformer-7.15.1.tgz",
- "integrity": "sha512-Hwp0swHmvN2D6iM67mnoP7wPeiDipz/GDdyO7CfnYjUsUQcRHzMouoqJ91dp9bO+f4EJ0Vr+8C1qIYRAn4ZDhA==",
- "license": "MIT",
- "dependencies": {
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "remark": "^15.0.1",
- "remark-parse": "^11.0.0",
- "remark-stringify": "^11.0.0",
- "tslib": "^2.8.1",
- "unified": "^11.0.3"
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/utils": {
- "version": "7.15.1",
- "resolved": "https://registry.npmjs.org/@milkdown/utils/-/utils-7.15.1.tgz",
- "integrity": "sha512-CvApKO84xdIGOUqvVeyDKRyN+PYqi8WNC9im7YWis2EojaSEleX7GMVOoWUHcB8xSdyuY+yJzPguMsx3QKuHIg==",
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
- "dependencies": {
- "@milkdown/core": "7.15.1",
- "@milkdown/ctx": "7.15.1",
- "@milkdown/exception": "7.15.1",
- "@milkdown/prose": "7.15.1",
- "@milkdown/transformer": "7.15.1",
- "nanoid": "^5.0.9",
- "tslib": "^2.8.1"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@milkdown/utils/node_modules/nanoid": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
- "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.js"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
- "engines": {
- "node": "^18 || >=20"
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"license": "MIT",
"dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
+ "@radix-ui/rect": "1.1.1"
},
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
- "engines": {
- "node": ">= 8"
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@polka/url": {
- "version": "1.0.0-next.29",
- "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
- "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
- "dev": true,
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@react-dnd/asap": {
@@ -2268,6 +3403,28 @@
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
"license": "MIT"
},
+ "node_modules/@react-hook/intersection-observer": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@react-hook/intersection-observer/-/intersection-observer-3.1.2.tgz",
+ "integrity": "sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-hook/passive-layout-effect": "^1.2.0",
+ "intersection-observer": "^0.10.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/@react-hook/passive-layout-effect": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz",
+ "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
@@ -2571,6 +3728,65 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@stitches/core": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
+ "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.85.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.7.tgz",
+ "integrity": "sha512-FLT3EtuTbXBmOrDku4bI80Eivmjn/o/Zc1lVEd/6yzR8UAUSnDwYiwghCZvLqHyGSN5mO35ux1yPGMFYBFRSwA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.84.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz",
+ "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.85.8",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.8.tgz",
+ "integrity": "sha512-r3rW55STAO03EJg5mrCVIJvaEK3oeHme5u7QovuRFIKRbEgTzTv2DPdenX46X+x56LsU3ree1N4rzI/+gJ7KEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.85.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.85.8",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.85.8.tgz",
+ "integrity": "sha512-83SXqRpmVlRMpaj32veez/8ohjY7O4VQIYDqW91b4i9AQjiYgE24FbBfR/SOL8b5MfKhHMZkD+BQSpCh9jY06w==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.84.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.85.8",
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -2777,55 +3993,6 @@
"@babel/types": "^7.20.7"
}
},
- "node_modules/@types/d3-color": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
- "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
- "license": "MIT"
- },
- "node_modules/@types/d3-drag": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
- "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-interpolate": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
- "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-color": "*"
- }
- },
- "node_modules/@types/d3-selection": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
- "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
- "license": "MIT"
- },
- "node_modules/@types/d3-transition": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
- "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-zoom": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
- "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-interpolate": "*",
- "@types/d3-selection": "*"
- }
- },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2839,9 +4006,17 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -2858,45 +4033,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/katex": {
- "version": "0.16.7",
- "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
- "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
- "license": "MIT"
- },
- "node_modules/@types/lodash": {
- "version": "4.17.20",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
- "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
- "license": "MIT"
- },
- "node_modules/@types/lodash-es": {
- "version": "4.17.12",
- "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
- "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
- "license": "MIT",
- "dependencies": {
- "@types/lodash": "*"
- }
- },
- "node_modules/@types/lodash.debounce": {
- "version": "4.0.9",
- "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
- "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
- "license": "MIT",
- "dependencies": {
- "@types/lodash": "*"
- }
- },
- "node_modules/@types/lodash.throttle": {
- "version": "4.1.9",
- "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz",
- "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==",
- "license": "MIT",
- "dependencies": {
- "@types/lodash": "*"
- }
- },
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -2926,14 +4062,12 @@
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -2944,26 +4078,19 @@
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@types/semver": {
- "version": "7.7.0",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
- "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
"dev": true,
"license": "MIT"
},
- "node_modules/@types/trusted-types": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "license": "MIT",
- "optional": true
- },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2971,33 +4098,34 @@
"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",
- "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+ "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/regexpp": "^4.4.0",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/type-utils": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/type-utils": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "natural-compare-lite": "^1.4.0",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^5.0.0",
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -3006,26 +4134,27 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
- "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -3034,17 +4163,17 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
- "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0"
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -3052,26 +4181,26 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
- "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+ "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "5.62.0",
- "@typescript-eslint/utils": "5.62.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
"debug": "^4.3.4",
- "tsutils": "^3.21.0"
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "*"
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -3080,13 +4209,13 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
- "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
"dev": true,
"license": "MIT",
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -3094,72 +4223,98 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
- "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/visitor-keys": "5.62.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "semver": "^7.3.7",
- "tsutils": "^3.21.0"
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": ">=16 || 14 >=14.17"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
- "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@types/json-schema": "^7.0.9",
- "@types/semver": "^7.3.12",
- "@typescript-eslint/scope-manager": "5.62.0",
- "@typescript-eslint/types": "5.62.0",
- "@typescript-eslint/typescript-estree": "5.62.0",
- "eslint-scope": "^5.1.1",
- "semver": "^7.3.7"
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "semver": "^7.5.4"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "eslint": "^7.0.0 || ^8.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "5.62.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
- "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "5.62.0",
- "eslint-visitor-keys": "^3.3.0"
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -3170,7 +4325,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
- "dev": true,
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
@@ -3417,165 +4571,10 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@vue/compiler-core": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
- "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.28.0",
- "@vue/shared": "3.5.18",
- "entities": "^4.5.0",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/@vue/compiler-core/node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/@vue/compiler-core/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/@vue/compiler-dom": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz",
- "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-core": "3.5.18",
- "@vue/shared": "3.5.18"
- }
- },
- "node_modules/@vue/compiler-sfc": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz",
- "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.28.0",
- "@vue/compiler-core": "3.5.18",
- "@vue/compiler-dom": "3.5.18",
- "@vue/compiler-ssr": "3.5.18",
- "@vue/shared": "3.5.18",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.30.17",
- "postcss": "^8.5.6",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/@vue/compiler-ssr": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz",
- "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.18",
- "@vue/shared": "3.5.18"
- }
- },
- "node_modules/@vue/reactivity": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz",
- "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==",
- "license": "MIT",
- "dependencies": {
- "@vue/shared": "3.5.18"
- }
- },
- "node_modules/@vue/runtime-core": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz",
- "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==",
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.18",
- "@vue/shared": "3.5.18"
- }
- },
- "node_modules/@vue/runtime-dom": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz",
- "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==",
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.18",
- "@vue/runtime-core": "3.5.18",
- "@vue/shared": "3.5.18",
- "csstype": "^3.1.3"
- }
- },
- "node_modules/@vue/server-renderer": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz",
- "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-ssr": "3.5.18",
- "@vue/shared": "3.5.18"
- },
- "peerDependencies": {
- "vue": "3.5.18"
- }
- },
- "node_modules/@vue/shared": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz",
- "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
- "license": "MIT"
- },
- "node_modules/@xyflow/react": {
- "version": "12.6.4",
- "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.6.4.tgz",
- "integrity": "sha512-/dOQ43Nu217cwHzy7f8kNUrFMeJJENzftVgT2VdFFHi6fHlG83pF+gLmvkRW9Be7alCsR6G+LFxxCdsQQbazHg==",
- "license": "MIT",
- "dependencies": {
- "@xyflow/system": "0.0.61",
- "classcat": "^5.0.3",
- "zustand": "^4.4.0"
- },
- "peerDependencies": {
- "react": ">=17",
- "react-dom": ">=17"
- }
- },
- "node_modules/@xyflow/system": {
- "version": "0.0.61",
- "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.61.tgz",
- "integrity": "sha512-TsZG/Ez8dzxX6/Ol44LvFqVZsYvyz6dpDlAQZZk6hTL7JLGO5vN3dboRJqMwU8/Qtr5IEv5YBzojjAwIqW1HCA==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-drag": "^3.0.7",
- "@types/d3-selection": "^3.0.10",
- "@types/d3-transition": "^3.0.8",
- "@types/d3-zoom": "^3.0.8",
- "d3-drag": "^3.0.0",
- "d3-selection": "^3.0.0",
- "d3-zoom": "^3.0.0"
- }
- },
"node_modules/acorn": {
- "version": "8.14.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
- "dev": true,
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -3588,7 +4587,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -3634,6 +4632,12 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/anser": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.2.tgz",
+ "integrity": "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==",
+ "license": "MIT"
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3692,9 +4696,20 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -3820,6 +4835,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3890,6 +4925,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -4047,6 +5106,36 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -4098,10 +5187,16 @@
"node": ">= 6"
}
},
- "node_modules/classcat": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
- "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
+ "node_modules/clean-set": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/clean-set/-/clean-set-1.1.2.tgz",
+ "integrity": "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==",
"license": "MIT"
},
"node_modules/clsx": {
@@ -4113,6 +5208,18 @@
"node": ">=6"
}
},
+ "node_modules/cm6-theme-basic-light": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/cm6-theme-basic-light/-/cm6-theme-basic-light-0.2.0.tgz",
+ "integrity": "sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
@@ -4161,6 +5268,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -4171,6 +5288,12 @@
"node": ">= 6"
}
},
+ "node_modules/compute-scroll-into-view": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+ "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g==",
+ "license": "MIT"
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4217,159 +5340,67 @@
"which": "^2.0.1"
},
"engines": {
- "node": ">= 8"
- }
- },
- "node_modules/css.escape": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
- "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/cssstyle": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz",
- "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@asamuzakjp/css-color": "^3.1.2",
- "rrweb-cssom": "^0.8.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/cssstyle/node_modules/rrweb-cssom": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
- "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
- },
- "node_modules/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-dispatch": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
- "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-drag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
- "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
- "license": "ISC",
- "dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-selection": "3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-ease": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
- "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
- "license": "ISC",
- "dependencies": {
- "d3-color": "1 - 3"
- },
- "engines": {
- "node": ">=12"
+ "node": ">= 8"
}
},
- "node_modules/d3-selection": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
- "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/d3-timer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
- "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
- "license": "ISC",
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=4"
}
},
- "node_modules/d3-transition": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
- "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
- "license": "ISC",
+ "node_modules/cssstyle": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz",
+ "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "d3-color": "1 - 3",
- "d3-dispatch": "1 - 3",
- "d3-ease": "1 - 3",
- "d3-interpolate": "1 - 3",
- "d3-timer": "1 - 3"
+ "@asamuzakjp/css-color": "^3.1.2",
+ "rrweb-cssom": "^0.8.0"
},
"engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "d3-selection": "2 - 3"
+ "node": ">=18"
}
},
- "node_modules/d3-zoom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
- "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/d": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+ "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-drag": "2 - 3",
- "d3-interpolate": "1 - 3",
- "d3-selection": "2 - 3",
- "d3-transition": "2 - 3"
+ "es5-ext": "^0.10.64",
+ "type": "^2.7.2"
},
"engines": {
- "node": ">=12"
+ "node": ">=0.12"
}
},
"node_modules/data-urls": {
@@ -4541,6 +5572,12 @@
"node": ">=6"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -4632,13 +5669,32 @@
"dev": true,
"license": "MIT"
},
- "node_modules/dompurify": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
- "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
- "license": "(MPL-2.0 OR Apache-2.0)",
- "optionalDependencies": {
- "@types/trusted-types": "^2.0.7"
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/downshift": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.2.tgz",
+ "integrity": "sha512-iOv+E1Hyt3JDdL9yYcOgW7nZ7GQ2Uz6YbggwXvKUSleetYhU2nXD482Rz6CzvM4lvI1At34BYruKAL4swRGxaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.14.8",
+ "compute-scroll-into-view": "^2.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.12.0"
}
},
"node_modules/dunder-proto": {
@@ -4760,6 +5816,46 @@
"node": ">= 0.4"
}
},
+ "node_modules/es5-ext": {
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+ "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.2",
+ "ext": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -4809,6 +5905,12 @@
"node": ">=6"
}
},
+ "node_modules/escape-carriage": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
+ "integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
+ "license": "MIT"
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -4903,17 +6005,20 @@
}
},
"node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
+ "estraverse": "^5.2.0"
},
"engines": {
- "node": ">=8.0.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
@@ -4929,33 +6034,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/eslint/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -4972,6 +6050,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -5003,16 +6096,6 @@
"node": ">=0.10"
}
},
- "node_modules/esquery/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -5026,7 +6109,7 @@
"node": ">=4.0"
}
},
- "node_modules/esrecurse/node_modules/estraverse": {
+ "node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
@@ -5036,14 +6119,28 @@
"node": ">=4.0"
}
},
- "node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-visit": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz",
+ "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
"node_modules/estree-walker": {
@@ -5066,6 +6163,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -5090,6 +6197,15 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+ "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+ "license": "ISC",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5156,6 +6272,19 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fault": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz",
+ "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
@@ -5277,6 +6406,14 @@
"node": ">= 6"
}
},
+ "node_modules/format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -5414,6 +6551,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -5605,6 +6751,46 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -5640,6 +6826,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -5691,6 +6887,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5757,6 +6973,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
+ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
+ "license": "MIT"
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -5768,8 +6990,38 @@
"hasown": "^2.0.2",
"side-channel": "^1.1.0"
},
- "engines": {
- "node": ">= 0.4"
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/intersection-observer": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz",
+ "integrity": "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==",
+ "license": "W3C-20150513"
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-arguments": {
@@ -5899,6 +7151,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5932,6 +7194,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -6141,6 +7413,17 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/isomorphic.js": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
+ "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -6231,7 +7514,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -6328,31 +7610,6 @@
"node": ">=6"
}
},
- "node_modules/katex": {
- "version": "0.16.22",
- "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
- "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
- "funding": [
- "https://opencollective.com/katex",
- "https://github.com/sponsors/katex"
- ],
- "license": "MIT",
- "dependencies": {
- "commander": "^8.3.0"
- },
- "bin": {
- "katex": "cli.js"
- }
- },
- "node_modules/katex/node_modules/commander": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6363,6 +7620,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -6377,6 +7643,34 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lexical": {
+ "version": "0.33.1",
+ "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.33.1.tgz",
+ "integrity": "sha512-+kiCS/GshQmCs/meMb8MQT4AMvw3S3Ef0lSCv2Xi6Itvs59OD+NjQWNfYkDteIbKtVE/w0Yiqh56VyGwIb8UcA==",
+ "license": "MIT"
+ },
+ "node_modules/lib0": {
+ "version": "0.2.114",
+ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
+ "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "isomorphic.js": "^0.2.4"
+ },
+ "bin": {
+ "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
+ "0gentesthtml": "bin/gentesthtml.js",
+ "0serve": "bin/0serve.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -6437,18 +7731,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash-es": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
- "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
- "license": "MIT"
- },
- "node_modules/lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "license": "MIT"
- },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -6456,12 +7738,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash.throttle": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
- "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
- "license": "MIT"
- },
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -6517,7 +7793,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
- "dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -6527,6 +7802,7 @@
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
@@ -6587,30 +7863,20 @@
"node": ">= 0.4"
}
},
- "node_modules/mdast-util-definitions": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
- "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==",
+ "node_modules/mdast-util-directive": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz",
+ "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"@types/unist": "^3.0.0",
- "unist-util-visit": "^5.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/mdast-util-find-and-replace": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
- "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
- "license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "escape-string-regexp": "^5.0.0",
- "unist-util-is": "^6.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
@@ -6618,18 +7884,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
- "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/mdast-util-from-markdown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -6654,57 +7908,34 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-gfm": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
- "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
- "license": "MIT",
- "dependencies": {
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-gfm-autolink-literal": "^2.0.0",
- "mdast-util-gfm-footnote": "^2.0.0",
- "mdast-util-gfm-strikethrough": "^2.0.0",
- "mdast-util-gfm-table": "^2.0.0",
- "mdast-util-gfm-task-list-item": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/mdast-util-gfm-autolink-literal": {
+ "node_modules/mdast-util-frontmatter": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
- "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz",
+ "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
- "ccount": "^2.0.0",
"devlop": "^1.0.0",
- "mdast-util-find-and-replace": "^3.0.0",
- "micromark-util-character": "^2.0.0"
+ "escape-string-regexp": "^5.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-extension-frontmatter": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-gfm-footnote": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
- "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "devlop": "^1.1.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0"
+ "engines": {
+ "node": ">=12"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mdast-util-gfm-strikethrough": {
@@ -6755,19 +7986,86 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-math": {
+ "node_modules/mdast-util-highlight-mark": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-highlight-mark/-/mdast-util-highlight-mark-1.2.2.tgz",
+ "integrity": "sha512-OYumVoytj+B9YgwzBhBcYUCLYHIPvJtAvwnMyKhUXbfUFuER5S+FDZyu9fadUxm2TCT5fRYK3jQXh2ioWAxrMw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-highlight-mark": "1.2.0"
+ }
+ },
+ "node_modules/mdast-util-mdx": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
- "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
+ "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
"license": "MIT",
"dependencies": {
+ "@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
- "longest-streak": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.1.0",
- "unist-util-remove-position": "^5.0.0"
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
@@ -6788,6 +8086,27 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-to-markdown": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
@@ -6908,19 +8227,34 @@
"micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-extension-gfm": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
- "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "node_modules/micromark-extension-directive": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz",
+ "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==",
"license": "MIT",
"dependencies": {
- "micromark-extension-gfm-autolink-literal": "^2.0.0",
- "micromark-extension-gfm-footnote": "^2.0.0",
- "micromark-extension-gfm-strikethrough": "^2.0.0",
- "micromark-extension-gfm-table": "^2.0.0",
- "micromark-extension-gfm-tagfilter": "^2.0.0",
- "micromark-extension-gfm-task-list-item": "^2.0.0",
- "micromark-util-combine-extensions": "^2.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "parse-entities": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-frontmatter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz",
+ "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==",
+ "license": "MIT",
+ "dependencies": {
+ "fault": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
@@ -6928,14 +8262,33 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-gfm-autolink-literal": {
+ "node_modules/micromark-extension-gfm-strikethrough": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
- "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
"license": "MIT",
"dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
- "micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
@@ -6944,18 +8297,15 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-gfm-footnote": {
+ "node_modules/micromark-extension-gfm-task-list-item": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
- "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
- "micromark-core-commonmark": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0",
- "micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
@@ -6964,45 +8314,72 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-gfm-strikethrough": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
- "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "node_modules/micromark-extension-highlight-mark": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-highlight-mark/-/micromark-extension-highlight-mark-1.2.0.tgz",
+ "integrity": "sha512-huGtbd/9kQsMk8u7nrVMaS5qH/47yDG6ZADggo5Owz5JoY8wdfQjfuy118/QiYNCvdFuFDbzT0A7K7Hp2cBsXA==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "uvu": "^0.5.6"
+ }
+ },
+ "node_modules/micromark-extension-mdx-expression": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
+ "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
+ "@types/estree": "^1.0.0",
"devlop": "^1.0.0",
- "micromark-util-chunked": "^2.0.0",
- "micromark-util-classify-character": "^2.0.0",
- "micromark-util-resolve-all": "^2.0.0",
+ "micromark-factory-mdx-expression": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-gfm-table": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
- "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "node_modules/micromark-extension-mdx-jsx": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz",
+ "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==",
"license": "MIT",
"dependencies": {
+ "@types/estree": "^1.0.0",
"devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "micromark-factory-mdx-expression": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "micromark-util-types": "^2.0.0",
+ "vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-gfm-tagfilter": {
+ "node_modules/micromark-extension-mdx-md": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
- "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz",
+ "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==",
"license": "MIT",
"dependencies": {
"micromark-util-types": "^2.0.0"
@@ -7012,16 +8389,19 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-gfm-task-list-item": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
- "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "node_modules/micromark-extension-mdxjs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz",
+ "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==",
"license": "MIT",
"dependencies": {
- "devlop": "^1.0.0",
- "micromark-factory-space": "^2.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
+ "acorn": "^8.0.0",
+ "acorn-jsx": "^5.0.0",
+ "micromark-extension-mdx-expression": "^3.0.0",
+ "micromark-extension-mdx-jsx": "^3.0.0",
+ "micromark-extension-mdx-md": "^2.0.0",
+ "micromark-extension-mdxjs-esm": "^3.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
@@ -7029,19 +8409,21 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-extension-math": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
- "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
+ "node_modules/micromark-extension-mdxjs-esm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz",
+ "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==",
"license": "MIT",
"dependencies": {
- "@types/katex": "^0.16.0",
+ "@types/estree": "^1.0.0",
"devlop": "^1.0.0",
- "katex": "^0.16.0",
- "micromark-factory-space": "^2.0.0",
+ "micromark-core-commonmark": "^2.0.0",
"micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "micromark-util-types": "^2.0.0",
+ "unist-util-position-from-estree": "^2.0.0",
+ "vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
@@ -7091,6 +8473,33 @@
"micromark-util-types": "^2.0.0"
}
},
+ "node_modules/micromark-factory-mdx-expression": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz",
+ "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-position-from-estree": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ }
+ },
"node_modules/micromark-factory-space": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
@@ -7292,6 +8701,31 @@
],
"license": "MIT"
},
+ "node_modules/micromark-util-events-to-acorn": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz",
+ "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-visit": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ }
+ },
"node_modules/micromark-util-html-tag-name": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
@@ -7439,7 +8873,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7539,6 +8972,15 @@
"integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
"license": "MIT"
},
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -7571,6 +9013,7 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -7592,12 +9035,11 @@
"dev": true,
"license": "MIT"
},
- "node_modules/natural-compare-lite": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
- "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
- "dev": true,
- "license": "MIT"
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
+ "license": "ISC"
},
"node_modules/node-releases": {
"version": "2.0.19",
@@ -7666,7 +9108,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -7787,10 +9228,10 @@
"node": ">= 0.8.0"
}
},
- "node_modules/orderedmap": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
- "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
+ "node_modules/outvariant": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz",
+ "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
"license": "MIT"
},
"node_modules/p-limit": {
@@ -7845,6 +9286,31 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -7950,6 +9416,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -8018,6 +9485,7 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -8210,181 +9678,31 @@
"node": ">=6"
}
},
- "node_modules/prosemirror-changeset": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
- "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-transform": "^1.0.0"
- }
- },
- "node_modules/prosemirror-commands": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
- "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-model": "^1.0.0",
- "prosemirror-state": "^1.0.0",
- "prosemirror-transform": "^1.10.2"
- }
- },
- "node_modules/prosemirror-dropcursor": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
- "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-state": "^1.0.0",
- "prosemirror-transform": "^1.1.0",
- "prosemirror-view": "^1.1.0"
- }
- },
- "node_modules/prosemirror-gapcursor": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
- "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-keymap": "^1.0.0",
- "prosemirror-model": "^1.0.0",
- "prosemirror-state": "^1.0.0",
- "prosemirror-view": "^1.0.0"
- }
- },
- "node_modules/prosemirror-history": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
- "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-state": "^1.2.2",
- "prosemirror-transform": "^1.0.0",
- "prosemirror-view": "^1.31.0",
- "rope-sequence": "^1.3.0"
- }
- },
- "node_modules/prosemirror-inputrules": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz",
- "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-state": "^1.0.0",
- "prosemirror-transform": "^1.0.0"
- }
- },
- "node_modules/prosemirror-keymap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
- "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-state": "^1.0.0",
- "w3c-keyname": "^2.2.0"
- }
- },
- "node_modules/prosemirror-model": {
- "version": "1.25.1",
- "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.1.tgz",
- "integrity": "sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==",
- "license": "MIT",
- "dependencies": {
- "orderedmap": "^2.0.0"
- }
- },
- "node_modules/prosemirror-safari-ime-span": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/prosemirror-safari-ime-span/-/prosemirror-safari-ime-span-1.0.2.tgz",
- "integrity": "sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-state": "^1.4.3",
- "prosemirror-view": "^1.33.8"
- },
- "funding": {
- "url": "https://github.com/sponsors/ocavue"
- }
- },
- "node_modules/prosemirror-schema-list": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
- "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-model": "^1.0.0",
- "prosemirror-state": "^1.0.0",
- "prosemirror-transform": "^1.7.3"
- }
- },
- "node_modules/prosemirror-state": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
- "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-model": "^1.0.0",
- "prosemirror-transform": "^1.0.0",
- "prosemirror-view": "^1.27.0"
- }
- },
- "node_modules/prosemirror-tables": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz",
- "integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-keymap": "^1.2.2",
- "prosemirror-model": "^1.25.0",
- "prosemirror-state": "^1.4.3",
- "prosemirror-transform": "^1.10.3",
- "prosemirror-view": "^1.39.1"
- }
- },
- "node_modules/prosemirror-transform": {
- "version": "1.10.4",
- "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz",
- "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==",
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
- "prosemirror-model": "^1.21.0"
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
}
},
- "node_modules/prosemirror-view": {
- "version": "1.40.0",
- "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.0.tgz",
- "integrity": "sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==",
- "license": "MIT",
- "dependencies": {
- "prosemirror-model": "^1.20.0",
- "prosemirror-state": "^1.0.0",
- "prosemirror-transform": "^1.1.0"
- }
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
},
- "node_modules/prosemirror-virtual-cursor": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/prosemirror-virtual-cursor/-/prosemirror-virtual-cursor-0.4.2.tgz",
- "integrity": "sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg==",
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
- "url": "https://github.com/sponsors/ocavue"
- },
- "peerDependencies": {
- "prosemirror-model": "^1.0.0",
- "prosemirror-state": "^1.0.0",
- "prosemirror-view": "^1.0.0"
- },
- "peerDependenciesMeta": {
- "prosemirror-model": {
- "optional": true
- },
- "prosemirror-state": {
- "optional": true
- },
- "prosemirror-view": {
- "optional": true
- }
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/psl": {
@@ -8450,6 +9768,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-devtools-inline": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/react-devtools-inline/-/react-devtools-inline-4.4.0.tgz",
+ "integrity": "sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es6-symbol": "^3"
+ }
+ },
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
@@ -8502,13 +9829,71 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+ "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.62.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
+ "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -8516,7 +9901,54 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
"node_modules/react-router": {
@@ -8551,6 +9983,28 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -8618,71 +10072,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/remark": {
- "version": "15.0.1",
- "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz",
- "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==",
- "license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "remark-parse": "^11.0.0",
- "remark-stringify": "^11.0.0",
- "unified": "^11.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-gfm": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
- "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
- "license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-gfm": "^3.0.0",
- "micromark-extension-gfm": "^3.0.0",
- "remark-parse": "^11.0.0",
- "remark-stringify": "^11.0.0",
- "unified": "^11.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-inline-links": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/remark-inline-links/-/remark-inline-links-7.0.0.tgz",
- "integrity": "sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==",
- "license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-definitions": "^6.0.0",
- "unist-util-visit": "^5.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-math": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
- "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
- "license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-math": "^3.0.0",
- "micromark-extension-math": "^3.0.0",
- "unified": "^11.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -8699,15 +10088,17 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/remark-stringify": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
- "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
"license": "MIT",
"dependencies": {
+ "@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
- "mdast-util-to-markdown": "^2.0.0",
- "unified": "^11.0.0"
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
@@ -8820,12 +10211,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/rope-sequence": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
- "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
- "license": "MIT"
- },
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -8857,6 +10242,18 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "license": "MIT",
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
@@ -9099,11 +10496,22 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -9111,6 +10519,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/static-browser-server": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz",
+ "integrity": "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.1.0",
+ "dotenv": "^16.0.3",
+ "mime-db": "^1.52.0",
+ "outvariant": "^1.3.0"
+ }
+ },
"node_modules/std-env": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
@@ -9132,6 +10552,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/strict-event-emitter": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz",
+ "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
+ "license": "MIT"
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -9202,6 +10628,20 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -9294,6 +10734,24 @@
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
+ "node_modules/style-to-js": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
+ "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.9"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz",
+ "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.4"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -9397,6 +10855,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "license": "MIT"
+ },
"node_modules/tailwind-merge": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz",
@@ -9569,6 +11033,16 @@
"node": ">=18"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
@@ -9579,6 +11053,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -9643,28 +11130,11 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/tsutils": {
- "version": "3.21.0",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
- "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tslib": "^1.8.1"
- },
- "engines": {
- "node": ">= 6"
- },
- "peerDependencies": {
- "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
- }
- },
- "node_modules/tsutils/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "dev": true,
- "license": "0BSD"
+ "node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+ "license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -9706,7 +11176,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -9730,6 +11200,24 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/unidiff": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/unidiff/-/unidiff-1.0.4.tgz",
+ "integrity": "sha512-ynU0vsAXw0ir8roa+xPCUHmnJ5goc5BTM2Kuc3IJd8UwgaeRs7VSD5+eeaQL+xp1JtB92hu/Zy/Lgy7RZcr1pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "diff": "^5.1.0"
+ }
+ },
+ "node_modules/unidiff/node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -9762,14 +11250,26 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-remove-position": {
+ "node_modules/unist-util-position": {
"version": "5.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
- "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"license": "MIT",
"dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-visit": "^5.0.0"
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position-from-estree": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz",
+ "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
@@ -9880,13 +11380,47 @@
"requires-port": "^1.0.0"
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
- "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
"node_modules/util-deprecate": {
@@ -9896,6 +11430,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/uvu": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
+ "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0",
+ "diff": "^5.0.0",
+ "kleur": "^4.0.3",
+ "sade": "^1.7.3"
+ },
+ "bin": {
+ "uvu": "bin.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/uvu/node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"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",
@@ -10080,27 +11641,6 @@
}
}
},
- "node_modules/vue": {
- "version": "3.5.18",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz",
- "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.18",
- "@vue/compiler-sfc": "3.5.18",
- "@vue/runtime-dom": "3.5.18",
- "@vue/server-renderer": "3.5.18",
- "@vue/shared": "3.5.18"
- },
- "peerDependencies": {
- "typescript": "*"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
@@ -10438,6 +11978,24 @@
"node": ">= 14.6"
}
},
+ "node_modules/yjs": {
+ "version": "13.6.27",
+ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
+ "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "lib0": "^0.2.99"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8.0.0"
+ },
+ "funding": {
+ "type": "GitHub Sponsors ❤",
+ "url": "https://github.com/sponsors/dmonad"
+ }
+ },
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
@@ -10470,34 +12028,6 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
- "node_modules/zustand": {
- "version": "4.5.7",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
- "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
- "license": "MIT",
- "dependencies": {
- "use-sync-external-store": "^1.2.2"
- },
- "engines": {
- "node": ">=12.7.0"
- },
- "peerDependencies": {
- "@types/react": ">=16.8",
- "immer": ">=9.0.6",
- "react": ">=16.8"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "immer": {
- "optional": true
- },
- "react": {
- "optional": true
- }
- }
- },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/archon-ui-main/package.json b/archon-ui-main/package.json
index 1f5a91c8a6..336132fee2 100644
--- a/archon-ui-main/package.json
+++ b/archon-ui-main/package.json
@@ -7,6 +7,14 @@
"dev": "npx vite",
"build": "npx vite build",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
+ "lint:files": "eslint --ext .js,.jsx,.ts,.tsx",
+ "biome": "biome check",
+ "biome:fix": "biome check --write",
+ "biome:format": "biome format --write",
+ "biome:lint": "biome lint",
+ "biome:ai": "biome check --reporter=json",
+ "biome:ai-fix": "biome check --write --reporter=json",
+ "biome:ci": "biome ci",
"preview": "npx vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
@@ -18,11 +26,17 @@
"seed:projects": "node --loader ts-node/esm ../scripts/seed-project-data.ts"
},
"dependencies": {
- "@milkdown/crepe": "^7.5.0",
- "@milkdown/kit": "^7.5.0",
- "@milkdown/plugin-history": "^7.5.0",
- "@milkdown/preset-commonmark": "^7.5.0",
- "@xyflow/react": "^12.3.0",
+ "@mdxeditor/editor": "^3.42.0",
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toast": "^1.2.15",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-query": "^5.85.8",
+ "@tanstack/react-query-devtools": "^5.85.8",
"clsx": "latest",
"date-fns": "^4.1.0",
"fractional-indexing": "^3.2.0",
@@ -33,24 +47,26 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^6.26.2",
"tailwind-merge": "latest",
"zod": "^3.25.46"
},
"devDependencies": {
+ "@biomejs/biome": "2.2.2",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20.19.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
- "@typescript-eslint/eslint-plugin": "^5.54.0",
- "@typescript-eslint/parser": "^5.54.0",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"autoprefixer": "latest",
- "eslint": "^8.50.0",
+ "eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.1",
"jsdom": "^24.1.0",
diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx
index 427347cbcc..2a0cdc22f1 100644
--- a/archon-ui-main/src/App.tsx
+++ b/archon-ui-main/src/App.tsx
@@ -1,13 +1,17 @@
import { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { KnowledgeBasePage } from './pages/KnowledgeBasePage';
import { SettingsPage } from './pages/SettingsPage';
import { MCPPage } from './pages/MCPPage';
import { OnboardingPage } from './pages/OnboardingPage';
-import { MainLayout } from './components/layouts/MainLayout';
+import { MainLayout } from './components/layout/MainLayout';
import { ThemeProvider } from './contexts/ThemeContext';
import { ToastProvider } from './contexts/ToastContext';
+import { ToastProvider as FeaturesToastProvider } from './features/ui/components/ToastProvider';
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
+import { TooltipProvider } from './features/ui/primitives/tooltip';
import { ProjectPage } from './pages/ProjectPage';
import { DisconnectScreenOverlay } from './components/DisconnectScreenOverlay';
import { ErrorBoundaryWithBugReport } from './components/bug-report/ErrorBoundaryWithBugReport';
@@ -15,6 +19,28 @@ import { MigrationBanner } from './components/ui/MigrationBanner';
import { serverHealthService } from './services/serverHealthService';
import { useMigrationStatus } from './hooks/useMigrationStatus';
+// Create a client with optimized settings for our polling use case
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ // Keep data fresh for 2 seconds by default
+ staleTime: 2000,
+ // Cache data for 5 minutes
+ gcTime: 5 * 60 * 1000,
+ // Retry failed requests 3 times
+ retry: 3,
+ // Refetch on window focus
+ refetchOnWindowFocus: true,
+ // Don't refetch on reconnect by default (we handle this manually)
+ refetchOnReconnect: false,
+ },
+ mutations: {
+ // Retry mutations once on failure
+ retry: 1,
+ },
+ },
+});
+
const AppRoutes = () => {
const { projectsEnabled } = useSettings();
@@ -105,12 +131,21 @@ const AppContent = () => {
export function App() {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {import.meta.env.VITE_SHOW_DEVTOOLS === 'true' && (
+
+ )}
+
);
}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/layouts/ArchonChatPanel.tsx b/archon-ui-main/src/components/agent-chat/ArchonChatPanel.tsx
similarity index 100%
rename from archon-ui-main/src/components/layouts/ArchonChatPanel.tsx
rename to archon-ui-main/src/components/agent-chat/ArchonChatPanel.tsx
diff --git a/archon-ui-main/src/components/knowledge-base/CrawlingProgressCard.tsx b/archon-ui-main/src/components/knowledge-base/CrawlingProgressCard.tsx
index 0f39158101..f5eeb5aa71 100644
--- a/archon-ui-main/src/components/knowledge-base/CrawlingProgressCard.tsx
+++ b/archon-ui-main/src/components/knowledge-base/CrawlingProgressCard.tsx
@@ -26,7 +26,7 @@ import { Card } from '../ui/Card';
import { Button } from '../ui/Button';
import { Badge } from '../ui/Badge';
import { CrawlProgressData } from '../../types/crawl';
-import { useCrawlProgressPolling } from '../../hooks/usePolling';
+import { useCrawlProgressPolling } from '../../hooks/useCrawlQueries';
import { useTerminalScroll } from '../../hooks/useTerminalScroll';
interface CrawlingProgressCardProps {
diff --git a/archon-ui-main/src/components/layout/MainLayout.tsx b/archon-ui-main/src/components/layout/MainLayout.tsx
new file mode 100644
index 0000000000..da0b26964b
--- /dev/null
+++ b/archon-ui-main/src/components/layout/MainLayout.tsx
@@ -0,0 +1,193 @@
+import { AlertCircle, WifiOff } from "lucide-react";
+import type React from "react";
+import { useEffect } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { useToast } from "../../features/ui/hooks/useToast";
+import { cn } from "../../lib/utils";
+import { credentialsService } from "../../services/credentialsService";
+import { isLmConfigured } from "../../utils/onboarding";
+
+// TEMPORARY: Import from old components until they're migrated to features
+import { BackendStartupError } from "../BackendStartupError";
+import { useBackendHealth } from "./hooks/useBackendHealth";
+import { Navigation } from "./Navigation";
+
+interface MainLayoutProps {
+ children: React.ReactNode;
+ className?: string;
+}
+
+interface BackendStatusProps {
+ isHealthLoading: boolean;
+ isBackendError: boolean;
+ healthData: { ready: boolean } | undefined;
+}
+
+/**
+ * Backend health indicator component
+ */
+function BackendStatus({ isHealthLoading, isBackendError, healthData }: BackendStatusProps) {
+ if (isHealthLoading) {
+ return (
+
+ );
+ }
+
+ if (isBackendError) {
+ return (
+
+
+ Backend Offline
+
+ );
+ }
+
+ if (healthData?.ready === false) {
+ return (
+
+
+
Backend Starting...
+
+ );
+ }
+
+ return null;
+}
+
+/**
+ * Modern main layout using TanStack Query and Radix UI patterns
+ * Uses CSS Grid for layout instead of fixed positioning
+ */
+export function MainLayout({ children, className }: MainLayoutProps) {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { showToast } = useToast();
+
+ // Backend health monitoring with TanStack Query
+ const {
+ data: healthData,
+ isError: isBackendError,
+ error: backendError,
+ isLoading: isHealthLoading,
+ failureCount,
+ } = useBackendHealth();
+
+ // Track if backend has completely failed (for showing BackendStartupError)
+ const backendStartupFailed = isBackendError && failureCount >= 5;
+
+ // TEMPORARY: Handle onboarding redirect using old logic until migrated
+ useEffect(() => {
+ const checkOnboarding = async () => {
+ // Skip if backend failed to start
+ if (backendStartupFailed) {
+ return;
+ }
+
+ // Skip if not ready, already on onboarding, or already dismissed
+ if (!healthData?.ready || location.pathname === "/onboarding") {
+ return;
+ }
+
+ // Check if onboarding was already dismissed
+ if (localStorage.getItem("onboardingDismissed") === "true") {
+ return;
+ }
+
+ try {
+ // Fetch credentials in parallel (using old service temporarily)
+ const [ragCreds, apiKeyCreds] = await Promise.all([
+ credentialsService.getCredentialsByCategory("rag_strategy"),
+ credentialsService.getCredentialsByCategory("api_keys"),
+ ]);
+
+ // Check if LM is configured (using old utility temporarily)
+ const configured = isLmConfigured(ragCreds, apiKeyCreds);
+
+ if (!configured) {
+ // Redirect to onboarding
+ navigate("/onboarding", { replace: true });
+ }
+ } catch (error) {
+ // Log error but don't block app
+ console.error("ONBOARDING_CHECK_FAILED:", error);
+ showToast(`Configuration check failed. You can manually configure in Settings.`, "warning");
+ }
+ };
+
+ checkOnboarding();
+ }, [healthData?.ready, backendStartupFailed, location.pathname, navigate, showToast]);
+
+ // Show backend error toast (once)
+ useEffect(() => {
+ if (isBackendError && backendError) {
+ const errorMessage = backendError instanceof Error ? backendError.message : "Backend connection failed";
+ showToast(`Backend unavailable: ${errorMessage}. Some features may not work.`, "error");
+ }
+ }, [isBackendError, backendError, showToast]);
+
+ return (
+
+ {/* TEMPORARY: Show backend startup error using old component */}
+ {backendStartupFailed &&
}
+
+ {/* Fixed full-page background grid that doesn't scroll */}
+
+
+ {/* Floating Navigation */}
+
+
+
+
+
+ {/* Main Content Area - matches old layout exactly */}
+
+
+ {/* TEMPORARY: Floating Chat Button (disabled) - from old layout */}
+
+
+
+
+ {/* Tooltip */}
+
+
Coming Soon
+
Knowledge Assistant is under development
+
+
+
+
+ );
+}
+
+/**
+ * Layout variant without navigation for special pages
+ */
+export function MinimalLayout({ children, className }: MainLayoutProps) {
+ return (
+
+ {/* Background Grid Effect */}
+
+
+ {/* Centered Content */}
+
{children}
+
+ );
+}
diff --git a/archon-ui-main/src/components/layout/Navigation.tsx b/archon-ui-main/src/components/layout/Navigation.tsx
new file mode 100644
index 0000000000..e2f1e80676
--- /dev/null
+++ b/archon-ui-main/src/components/layout/Navigation.tsx
@@ -0,0 +1,178 @@
+import { BookOpen, Settings } from "lucide-react";
+import type React from "react";
+import { Link, useLocation } from "react-router-dom";
+// TEMPORARY: Use old SettingsContext until settings are migrated
+import { useSettings } from "../../contexts/SettingsContext";
+import { glassmorphism } from "../../features/ui/primitives/styles";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../../features/ui/primitives/tooltip";
+import { cn } from "../../lib/utils";
+
+interface NavigationItem {
+ path: string;
+ icon: React.ReactNode;
+ label: string;
+ enabled?: boolean;
+}
+
+interface NavigationProps {
+ className?: string;
+}
+
+/**
+ * Modern navigation component using Radix UI patterns
+ * No fixed positioning - parent controls layout
+ */
+export function Navigation({ className }: NavigationProps) {
+ const location = useLocation();
+ const { projectsEnabled } = useSettings();
+
+ // Navigation items configuration
+ const navigationItems: NavigationItem[] = [
+ {
+ path: "/",
+ icon: ,
+ label: "Knowledge Base",
+ enabled: true,
+ },
+ {
+ path: "/mcp",
+ icon: (
+
+
+
+
+ ),
+ label: "MCP Server",
+ enabled: true,
+ },
+ {
+ path: "/settings",
+ icon: ,
+ label: "Settings",
+ enabled: true,
+ },
+ ];
+
+ const isProjectsActive = location.pathname.startsWith("/projects");
+
+ return (
+
+ {/* Logo - Always visible, conditionally clickable for Projects */}
+
+
+ {projectsEnabled ? (
+
+
+ {/* Active state decorations */}
+ {isProjectsActive && (
+ <>
+
+
+ >
+ )}
+
+ ) : (
+
+
+
+ )}
+
+
+ {projectsEnabled ? "Project Management" : "Projects Disabled"}
+
+
+
+ {/* Separator */}
+
+
+ {/* Navigation Items */}
+
+ {navigationItems.map((item) => {
+ const isActive = location.pathname === item.path;
+ const isEnabled = item.enabled !== false;
+
+ return (
+
+
+ {
+ if (!isEnabled) {
+ e.preventDefault();
+ }
+ }}
+ >
+ {item.icon}
+ {/* Active state decorations with neon line */}
+ {isActive && (
+ <>
+
+
+ >
+ )}
+
+
+
+ {item.label}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts b/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
new file mode 100644
index 0000000000..de0b8aa049
--- /dev/null
+++ b/archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
@@ -0,0 +1,64 @@
+import { useQuery } from "@tanstack/react-query";
+import { getApiUrl } from "../../../config/api";
+
+interface HealthResponse {
+ ready: boolean;
+ message?: string;
+ server_status?: string;
+ credentials_status?: string;
+ database_status?: string;
+ uptime?: number;
+}
+
+/**
+ * Hook to monitor backend health status using TanStack Query
+ * Replaces the direct fetch polling in old MainLayout
+ */
+export function useBackendHealth() {
+ return useQuery({
+ queryKey: ["backend", "health"],
+ queryFn: async () => {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
+
+ try {
+ const response = await fetch(`${getApiUrl()}/api/health`, {
+ method: "GET",
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ throw new Error(`Health check failed: ${response.status}`);
+ }
+
+ return response.json();
+ } catch (error) {
+ clearTimeout(timeoutId);
+ if (error instanceof Error && error.name === "AbortError") {
+ throw new Error("Health check timeout (5s)");
+ }
+ throw error;
+ }
+ },
+ // Retry configuration for startup scenarios
+ retry: (failureCount) => {
+ // Keep retrying during startup, up to 5 times
+ if (failureCount < 5) {
+ return true;
+ }
+ return false;
+ },
+ retryDelay: (attemptIndex) => {
+ // Exponential backoff: 1.5s, 2.25s, 3.375s, etc.
+ return Math.min(1500 * 1.5 ** attemptIndex, 10000);
+ },
+ // Refetch every 30 seconds when healthy
+ refetchInterval: 30000,
+ // Keep trying to connect on window focus
+ refetchOnWindowFocus: true,
+ // Consider data fresh for 20 seconds
+ staleTime: 20000,
+ });
+}
diff --git a/archon-ui-main/src/components/layout/index.ts b/archon-ui-main/src/components/layout/index.ts
new file mode 100644
index 0000000000..10736d9b0a
--- /dev/null
+++ b/archon-ui-main/src/components/layout/index.ts
@@ -0,0 +1,3 @@
+export { useBackendHealth } from "./hooks/useBackendHealth";
+export { MainLayout, MinimalLayout } from "./MainLayout";
+export { Navigation } from "./Navigation";
diff --git a/archon-ui-main/src/components/layout/types.ts b/archon-ui-main/src/components/layout/types.ts
new file mode 100644
index 0000000000..38874bba18
--- /dev/null
+++ b/archon-ui-main/src/components/layout/types.ts
@@ -0,0 +1,26 @@
+export interface NavigationItem {
+ path: string;
+ icon: React.ReactNode;
+ label: string;
+ enabled?: boolean;
+}
+
+export interface HealthResponse {
+ ready: boolean;
+ message?: string;
+ server_status?: string;
+ credentials_status?: string;
+ database_status?: string;
+ uptime?: number;
+}
+
+export interface AppSettings {
+ projectsEnabled: boolean;
+ theme?: "light" | "dark" | "system";
+ // Add other settings as needed
+}
+
+export interface OnboardingCheckResult {
+ shouldShowOnboarding: boolean;
+ reason: "dismissed" | "missing_rag" | "missing_api_key" | null;
+}
diff --git a/archon-ui-main/src/components/layouts/MainLayout.tsx b/archon-ui-main/src/components/layouts/MainLayout.tsx
deleted file mode 100644
index acc9188a0e..0000000000
--- a/archon-ui-main/src/components/layouts/MainLayout.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useNavigate, useLocation } from 'react-router-dom';
-import { SideNavigation } from './SideNavigation';
-import { ArchonChatPanel } from './ArchonChatPanel';
-import { X } from 'lucide-react';
-import { useToast } from '../../contexts/ToastContext';
-import { credentialsService } from '../../services/credentialsService';
-import { isLmConfigured } from '../../utils/onboarding';
-import { BackendStartupError } from '../BackendStartupError';
-/**
- * Props for the MainLayout component
- */
-interface MainLayoutProps {
- children: React.ReactNode;
-}
-/**
- * MainLayout - The main layout component for the application
- *
- * This component provides the overall layout structure including:
- * - Side navigation
- * - Main content area
- * - Knowledge chat panel (slidable)
- */
-export const MainLayout: React.FC = ({
- children
-}) => {
- // State to track if chat panel is open
- const [isChatOpen, setIsChatOpen] = useState(false);
- const { showToast } = useToast();
- const navigate = useNavigate();
- const location = useLocation();
- const [backendReady, setBackendReady] = useState(false);
- const [backendStartupFailed, setBackendStartupFailed] = useState(false);
-
- // Check backend readiness
- useEffect(() => {
-
- const checkBackendHealth = async (retryCount = 0) => {
- const maxRetries = 3; // 3 retries total
- const retryDelay = 1500; // 1.5 seconds between retries
-
- try {
- // Create AbortController for proper timeout handling
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 5000);
-
- // Check if backend is responding with a simple health check
- const response = await fetch(`${credentialsService['baseUrl']}/api/health`, {
- method: 'GET',
- signal: controller.signal
- });
-
- clearTimeout(timeoutId);
-
- if (response.ok) {
- const healthData = await response.json();
- console.log('📋 Backend health check:', healthData);
-
- // Check if backend is truly ready (not just started)
- if (healthData.ready === true) {
- console.log('✅ Backend is fully initialized');
- setBackendReady(true);
- setBackendStartupFailed(false);
- } else {
- // Backend is starting up but not ready yet
- console.log(`🔄 Backend initializing... (attempt ${retryCount + 1}/${maxRetries}):`, healthData.message || 'Loading credentials...');
-
- // Retry with shorter interval during initialization
- if (retryCount < maxRetries) {
- setTimeout(() => {
- checkBackendHealth(retryCount + 1);
- }, retryDelay); // Constant 1.5s retry during initialization
- } else {
- console.warn('Backend initialization taking too long - proceeding anyway');
- // Don't mark as failed yet, just not fully ready
- setBackendReady(false);
- }
- }
- } else {
- throw new Error(`Backend health check failed: ${response.status}`);
- }
- } catch (error) {
- // Handle AbortError separately for timeout
- const errorMessage = error instanceof Error
- ? (error.name === 'AbortError' ? 'Request timeout (5s)' : error.message)
- : 'Unknown error';
- // Only log after first attempt to reduce noise during normal startup
- if (retryCount > 0) {
- console.log(`Backend not ready yet (attempt ${retryCount + 1}/${maxRetries}):`, errorMessage);
- }
-
- // Retry if we haven't exceeded max retries
- if (retryCount < maxRetries) {
- setTimeout(() => {
- checkBackendHealth(retryCount + 1);
- }, retryDelay * Math.pow(1.5, retryCount)); // Exponential backoff for connection errors
- } else {
- console.error('Backend startup failed after maximum retries - showing error message');
- setBackendReady(false);
- setBackendStartupFailed(true);
- }
- }
- };
-
-
- // Start the health check process
- setTimeout(() => {
- checkBackendHealth();
- }, 1000); // Wait 1 second for initial app startup
- }, []); // Empty deps - only run once on mount
-
- // Check for onboarding redirect after backend is ready
- useEffect(() => {
- const checkOnboarding = async () => {
- // Skip if backend failed to start
- if (backendStartupFailed) {
- return;
- }
-
- // Skip if not ready, already on onboarding, or already dismissed
- if (!backendReady || location.pathname === '/onboarding') {
- return;
- }
-
- // Check if onboarding was already dismissed
- if (localStorage.getItem('onboardingDismissed') === 'true') {
- return;
- }
-
- try {
- // Fetch credentials in parallel
- const [ragCreds, apiKeyCreds] = await Promise.all([
- credentialsService.getCredentialsByCategory('rag_strategy'),
- credentialsService.getCredentialsByCategory('api_keys')
- ]);
-
- // Check if LM is configured
- const configured = isLmConfigured(ragCreds, apiKeyCreds);
-
- if (!configured) {
- // Redirect to onboarding
- navigate('/onboarding', { replace: true });
- }
- } catch (error) {
- // Detailed error handling per alpha principles - fail loud but don't block
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
- const errorDetails = {
- context: 'Onboarding configuration check',
- pathname: location.pathname,
- error: errorMessage,
- timestamp: new Date().toISOString()
- };
-
- // Log with full context and stack trace
- console.error('ONBOARDING_CHECK_FAILED:', errorDetails, error);
-
- // Make error visible to user but don't block app functionality
- showToast(
- `Configuration check failed: ${errorMessage}. You can manually configure in Settings.`,
- 'warning'
- );
-
- // Let user continue - onboarding is optional, they can configure manually
- }
- };
-
- checkOnboarding();
- }, [backendReady, backendStartupFailed, location.pathname, navigate, showToast]);
-
- return
- {/* Show backend startup error if backend failed to start */}
- {backendStartupFailed &&
}
-
- {/* Fixed full-page background grid that doesn't scroll */}
-
- {/* Floating Navigation */}
-
-
-
- {/* Main Content Area - no left margin to allow grid to extend full width */}
-
- {/* Floating Chat Button - Only visible when chat is closed */}
- {!isChatOpen && (
-
-
-
-
- {/* Tooltip */}
-
-
Coming Soon
-
Knowledge Assistant is under development
-
-
-
- )}
- {/* Chat Sidebar - Slides in/out from right */}
-
- {/* Close button - Only visible when chat is open */}
- {isChatOpen &&
setIsChatOpen(false)} className="absolute -left-14 bottom-6 z-50 w-12 h-12 rounded-full flex items-center justify-center backdrop-blur-md bg-gradient-to-b from-white/10 to-black/30 dark:from-white/10 dark:to-black/30 from-pink-100/80 to-pink-50/60 border border-pink-200 dark:border-pink-500/30 shadow-[0_0_15px_rgba(236,72,153,0.2)] dark:shadow-[0_0_15px_rgba(236,72,153,0.5)] hover:shadow-[0_0_20px_rgba(236,72,153,0.4)] dark:hover:shadow-[0_0_20px_rgba(236,72,153,0.7)] transition-all duration-300" aria-label="Close Knowledge Assistant">
-
- }
- {/* Knowledge Chat Panel */}
-
-
-
;
-};
diff --git a/archon-ui-main/src/components/layouts/SideNavigation.tsx b/archon-ui-main/src/components/layouts/SideNavigation.tsx
deleted file mode 100644
index f4165032bf..0000000000
--- a/archon-ui-main/src/components/layouts/SideNavigation.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React, { useState } from 'react';
-import { Link, useLocation } from 'react-router-dom';
-import { BookOpen, HardDrive, Settings } from 'lucide-react';
-import { useSettings } from '../../contexts/SettingsContext';
-/**
- * Interface for navigation items
- */
-export interface NavigationItem {
- path: string;
- icon: React.ReactNode;
- label: string;
-}
-/**
- * Props for the SideNavigation component
- */
-interface SideNavigationProps {
- className?: string;
- 'data-id'?: string;
-}
-/**
- * Tooltip component for navigation items
- */
-const NavTooltip: React.FC<{
- show: boolean;
- label: string;
- position?: 'left' | 'right';
-}> = ({
- show,
- label,
- position = 'right'
-}) => {
- if (!show) return null;
- return ;
-};
-/**
- * SideNavigation - A vertical navigation component
- *
- * This component renders a navigation sidebar with icons and the application logo.
- * It highlights the active route and provides hover effects.
- */
-export const SideNavigation: React.FC = ({
- className = '',
- 'data-id': dataId
-}) => {
- // State to track which tooltip is currently visible
- const [activeTooltip, setActiveTooltip] = useState(null);
- const { projectsEnabled } = useSettings();
-
- // Default navigation items
- const navigationItems: NavigationItem[] = [{
- path: '/',
- icon: ,
- label: 'Knowledge Base'
- }, {
- path: '/mcp',
- icon: ,
- label: 'MCP Server'
- }, {
- path: '/settings',
- icon: ,
- label: 'Settings'
- }];
- // Logo configuration
- const logoSrc = "/logo-neon.png";
- const logoAlt = 'Knowledge Base Logo';
- // Get current location to determine active route
- const location = useLocation();
- const isProjectsActive = location.pathname === '/projects' && projectsEnabled;
-
- const logoClassName = `
- logo-container p-2 relative rounded-lg transition-all duration-300
- ${isProjectsActive ? 'bg-gradient-to-b from-white/20 to-white/5 dark:from-white/10 dark:to-black/20 shadow-[0_5px_15px_-5px_rgba(59,130,246,0.3)] dark:shadow-[0_5px_15px_-5px_rgba(59,130,246,0.5)] transform scale-110' : ''}
- ${projectsEnabled ? 'hover:bg-white/10 dark:hover:bg-white/5 cursor-pointer' : 'opacity-50 cursor-not-allowed'}
- `;
-
- return
- {/* Logo - Conditionally clickable based on Projects enabled */}
- {projectsEnabled ? (
-
setActiveTooltip('logo')}
- onMouseLeave={() => setActiveTooltip(null)}
- >
-
- {/* Active state decorations */}
- {isProjectsActive && <>
-
-
- >}
-
-
- ) : (
-
setActiveTooltip('logo')}
- onMouseLeave={() => setActiveTooltip(null)}
- >
-
-
-
- )}
- {/* Navigation links */}
-
- {navigationItems.map(item => {
- const isActive = location.pathname === item.path;
- return setActiveTooltip(item.path)} onMouseLeave={() => setActiveTooltip(null)} aria-label={item.label}>
- {/* Active state decorations - Modified to place neon line below button with adjusted width */}
- {isActive && <>
-
- {/* Neon line positioned below the button with reduced width to respect curved edges */}
-
- >}
- {item.icon}
- {/* Custom tooltip */}
-
- ;
- })}
-
-
;
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/mcp/ClientCard.tsx b/archon-ui-main/src/components/mcp/ClientCard.tsx
deleted file mode 100644
index 6ddbd059bb..0000000000
--- a/archon-ui-main/src/components/mcp/ClientCard.tsx
+++ /dev/null
@@ -1,508 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import { Server, Activity, Clock, ChevronRight, Hammer, Settings, Trash2, Plug, PlugZap } from 'lucide-react';
-import { Client } from './MCPClients';
-import { mcpClientService } from '../../services/mcpClientService';
-import { useToast } from '../../contexts/ToastContext';
-
-interface ClientCardProps {
- client: Client;
- onSelect: () => void;
- onEdit?: (client: Client) => void;
- onDelete?: (client: Client) => void;
- onConnectionChange?: () => void;
-}
-
-export const ClientCard = ({
- client,
- onSelect,
- onEdit,
- onDelete,
- onConnectionChange
-}: ClientCardProps) => {
- const [isFlipped, setIsFlipped] = useState(false);
- const [isHovered, setIsHovered] = useState(false);
- const [isConnecting, setIsConnecting] = useState(false);
- const particlesRef = useRef(null);
- const { showToast } = useToast();
-
- // Special styling for Archon client
- const isArchonClient = client.name.includes('Archon') || client.name.includes('archon');
-
- // Status-based styling
- const statusConfig = {
- online: {
- color: isArchonClient ? 'archon' : 'cyan',
- glow: isArchonClient ? 'shadow-[0_0_25px_rgba(59,130,246,0.7),0_0_15px_rgba(168,85,247,0.5)] dark:shadow-[0_0_35px_rgba(59,130,246,0.8),0_0_20px_rgba(168,85,247,0.7)]' : 'shadow-[0_0_15px_rgba(34,211,238,0.5)] dark:shadow-[0_0_20px_rgba(34,211,238,0.7)]',
- border: isArchonClient ? 'border-blue-400/60 dark:border-blue-500/60' : 'border-cyan-400/50 dark:border-cyan-500/40',
- badge: isArchonClient ? 'bg-blue-500/30 text-blue-400 border-blue-500/40' : 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
- pulse: isArchonClient ? 'bg-blue-400' : 'bg-cyan-400'
- },
- offline: {
- color: 'gray',
- glow: 'shadow-[0_0_15px_rgba(156,163,175,0.3)] dark:shadow-[0_0_15px_rgba(156,163,175,0.4)]',
- border: 'border-gray-400/30 dark:border-gray-600/30',
- badge: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
- pulse: 'bg-gray-400'
- },
- error: {
- color: 'pink',
- glow: 'shadow-[0_0_15px_rgba(236,72,153,0.5)] dark:shadow-[0_0_20px_rgba(236,72,153,0.7)]',
- border: 'border-pink-400/50 dark:border-pink-500/40',
- badge: 'bg-pink-500/20 text-pink-400 border-pink-500/30',
- pulse: 'bg-pink-400'
- }
- };
-
- // Handle mouse movement for bioluminescent effect
- useEffect(() => {
- if (!isArchonClient || !particlesRef.current) return;
-
- const currentMousePos = { x: 0, y: 0 };
- const glowOrganisms: HTMLDivElement[] = [];
- let isMousePresent = false;
-
- const createBioluminescentOrganism = (targetX: number, targetY: number, delay = 0) => {
- const organism = document.createElement('div');
- organism.className = 'absolute rounded-full pointer-events-none';
-
- const startX = targetX + (Math.random() - 0.5) * 100;
- const startY = targetY + (Math.random() - 0.5) * 100;
- const size = 8 + Math.random() * 12;
-
- organism.style.left = `${startX}px`;
- organism.style.top = `${startY}px`;
- organism.style.width = `${size}px`;
- organism.style.height = `${size}px`;
- organism.style.transform = 'translate(-50%, -50%)';
- organism.style.opacity = '0';
-
- const hues = [180, 200, 220, 240, 260, 280];
- const hue = hues[Math.floor(Math.random() * hues.length)];
-
- organism.style.background = 'transparent';
-
- organism.style.boxShadow = `
- 0 0 ${size * 2}px hsla(${hue}, 90%, 60%, 0.4),
- 0 0 ${size * 4}px hsla(${hue}, 80%, 50%, 0.25),
- 0 0 ${size * 6}px hsla(${hue}, 70%, 40%, 0.15),
- 0 0 ${size * 8}px hsla(${hue}, 60%, 30%, 0.08)
- `;
-
- organism.style.filter = `blur(${2 + Math.random() * 3}px) opacity(0.6)`;
-
- particlesRef.current?.appendChild(organism);
-
- setTimeout(() => {
- const duration = 1200 + Math.random() * 800;
-
- organism.style.transition = `all ${duration}ms cubic-bezier(0.2, 0.0, 0.1, 1)`;
- organism.style.left = `${targetX + (Math.random() - 0.5) * 50}px`;
- organism.style.top = `${targetY + (Math.random() - 0.5) * 50}px`;
- organism.style.opacity = '0.8';
- organism.style.transform = 'translate(-50%, -50%) scale(1.2)';
-
- setTimeout(() => {
- if (!isMousePresent) {
- organism.style.transition = `all 2500ms cubic-bezier(0.6, 0.0, 0.9, 1)`;
- organism.style.left = `${startX + (Math.random() - 0.5) * 300}px`;
- organism.style.top = `${startY + (Math.random() - 0.5) * 300}px`;
- organism.style.opacity = '0';
- organism.style.transform = 'translate(-50%, -50%) scale(0.2)';
- organism.style.filter = `blur(${8 + Math.random() * 5}px) opacity(0.2)`;
- }
- }, duration + 800);
-
- setTimeout(() => {
- if (particlesRef.current?.contains(organism)) {
- particlesRef.current.removeChild(organism);
- const index = glowOrganisms.indexOf(organism);
- if (index > -1) glowOrganisms.splice(index, 1);
- }
- }, duration + 2000);
-
- }, delay);
-
- return organism;
- };
-
- const spawnOrganismsTowardMouse = () => {
- if (!isMousePresent) return;
-
- const count = 3 + Math.random() * 4;
- for (let i = 0; i < count; i++) {
- const organism = createBioluminescentOrganism(
- currentMousePos.x,
- currentMousePos.y,
- i * 100
- );
- glowOrganisms.push(organism);
- }
- };
-
- const handleMouseEnter = () => {
- isMousePresent = true;
- clearInterval(ambientInterval);
- ambientInterval = setInterval(createAmbientGlow, 1500);
- };
-
- const handleMouseMove = (e: MouseEvent) => {
- if (!particlesRef.current) return;
-
- const rect = particlesRef.current.getBoundingClientRect();
- currentMousePos.x = e.clientX - rect.left;
- currentMousePos.y = e.clientY - rect.top;
-
- isMousePresent = true;
-
- if (Math.random() < 0.4) {
- spawnOrganismsTowardMouse();
- }
- };
-
- const handleMouseLeave = () => {
- setTimeout(() => {
- isMousePresent = false;
- clearInterval(ambientInterval);
- }, 800);
- };
-
- const createAmbientGlow = () => {
- if (!particlesRef.current || isMousePresent) return;
-
- const x = Math.random() * particlesRef.current.clientWidth;
- const y = Math.random() * particlesRef.current.clientHeight;
- const organism = createBioluminescentOrganism(x, y);
-
- organism.style.opacity = '0.3';
- organism.style.filter = `blur(${4 + Math.random() * 4}px) opacity(0.4)`;
- organism.style.animation = 'pulse 4s ease-in-out infinite';
- organism.style.transform = 'translate(-50%, -50%) scale(0.8)';
-
- glowOrganisms.push(organism);
- };
-
- let ambientInterval = setInterval(createAmbientGlow, 1500);
-
- const cardElement = particlesRef.current;
- cardElement.addEventListener('mouseenter', handleMouseEnter);
- cardElement.addEventListener('mousemove', handleMouseMove);
- cardElement.addEventListener('mouseleave', handleMouseLeave);
-
- return () => {
- cardElement.removeEventListener('mouseenter', handleMouseEnter);
- cardElement.removeEventListener('mousemove', handleMouseMove);
- cardElement.removeEventListener('mouseleave', handleMouseLeave);
- clearInterval(ambientInterval);
- };
- }, [isArchonClient]);
-
- const currentStatus = statusConfig[client.status];
-
- // Handle card flip
- const toggleFlip = (e: React.MouseEvent) => {
- e.stopPropagation();
- setIsFlipped(!isFlipped);
- };
-
- // Handle edit
- const handleEdit = (e: React.MouseEvent) => {
- e.stopPropagation();
- onEdit?.(client);
- };
-
- // Handle connect/disconnect
- const handleConnect = async (e: React.MouseEvent) => {
- e.stopPropagation();
- setIsConnecting(true);
-
- try {
- if (client.status === 'offline') {
- await mcpClientService.connectClient(client.id);
- showToast(`Connected to ${client.name}`, 'success');
- } else {
- await mcpClientService.disconnectClient(client.id);
- showToast(`Disconnected from ${client.name}`, 'success');
- }
-
- // The parent component should handle refreshing the client list
- // No need to reload the entire page
- onConnectionChange?.();
- } catch (error) {
- showToast(error instanceof Error ? error.message : 'Connection operation failed', 'error');
- } finally {
- setIsConnecting(false);
- }
- };
-
- // Special background for Archon client
- const archonBackground = isArchonClient ? 'bg-gradient-to-b from-white/80 via-blue-50/30 to-white/60 dark:from-white/10 dark:via-blue-900/10 dark:to-black/30' : 'bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30';
-
- return (
- setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
- >
-
- {/* Front Side */}
-
- {/* Particle container for Archon client */}
- {isArchonClient && (
-
- )}
-
- {/* Subtle aurora glow effect for Archon client */}
- {isArchonClient && (
-
- )}
-
- {/* Connect/Disconnect button */}
-
- {client.status === 'offline' ? (
-
- ) : (
-
- )}
-
-
- {/* Edit button - moved to be second from right */}
- {onEdit && (
-
-
-
- )}
-
- {/* Delete button - only for non-Archon clients */}
- {!isArchonClient && onDelete && (
-
{
- e.stopPropagation();
- onDelete(client);
- }}
- className="absolute top-3 right-[84px] p-1.5 rounded-full bg-red-200/50 dark:bg-red-900/50 hover:bg-red-300/50 dark:hover:bg-red-800/50 transition-colors transform hover:scale-110 transition-transform duration-200 z-20"
- title="Delete client"
- >
-
-
- )}
-
- {/* Client info */}
-
- {isArchonClient ? (
-
-
-
-
- ) : (
-
-
-
- )}
-
-
-
- {client.name}
-
-
- {client.ip}
-
-
-
-
-
-
-
- Last seen:
-
- {client.lastSeen}
-
-
-
-
-
Version:
-
- {client.version}
-
-
-
-
- Tools:
-
- {client.tools.length} available
-
-
-
- {/* Error message display */}
- {client.status === 'error' && client.lastError && (
-
-
-
-
-
Last Error:
-
- {client.lastError}
-
-
-
-
- )}
-
-
- {/* Status badge - moved to bottom left */}
-
-
-
-
-
-
- {client.status.charAt(0).toUpperCase() + client.status.slice(1)}
-
-
-
- {/* Tools button - with Hammer icon */}
-
-
-
-
-
- {/* Back Side */}
-
- {/* Subtle aurora glow effect for Archon client */}
- {isArchonClient && (
-
- )}
-
- {/* Connect/Disconnect button - also on back side */}
-
- {client.status === 'offline' ? (
-
- ) : (
-
- )}
-
-
- {/* Edit button - also on back side */}
- {onEdit && (
-
-
-
- )}
-
- {/* Delete button on back side - only for non-Archon clients */}
- {!isArchonClient && onDelete && (
-
{
- e.stopPropagation();
- onDelete(client);
- }}
- className="absolute top-3 right-[84px] p-1.5 rounded-full bg-red-200/50 dark:bg-red-900/50 hover:bg-red-300/50 dark:hover:bg-red-800/50 transition-colors transform hover:scale-110 transition-transform duration-200 z-20"
- title="Delete client"
- >
-
-
- )}
-
-
-
- Available Tools ({client.tools.length})
-
-
-
- {client.tools.length === 0 ? (
-
-
- {client.status === 'offline'
- ? 'Client offline - tools unavailable'
- : 'No tools discovered'}
-
-
- ) : (
- client.tools.map(tool => (
-
-
-
- {tool.name}
-
-
-
-
- {tool.description}
-
- {tool.parameters.length > 0 && (
-
- {tool.parameters.length} parameter{tool.parameters.length !== 1 ? 's' : ''}
-
- )}
-
- ))
- )}
-
-
- {/* Status badge - also at bottom left on back side */}
-
-
-
-
-
-
- {client.status.charAt(0).toUpperCase() + client.status.slice(1)}
-
-
-
- {/* Flip button - back to front */}
-
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/mcp/MCPClients.tsx b/archon-ui-main/src/components/mcp/MCPClients.tsx
deleted file mode 100644
index d780ce62bb..0000000000
--- a/archon-ui-main/src/components/mcp/MCPClients.tsx
+++ /dev/null
@@ -1,858 +0,0 @@
-import React, { useState, memo, useEffect } from 'react';
-import { Plus, Settings, Trash2, X } from 'lucide-react';
-import { ClientCard } from './ClientCard';
-import { ToolTestingPanel } from './ToolTestingPanel';
-import { Button } from '../ui/Button';
-import { mcpClientService, MCPClient, MCPClientConfig } from '../../services/mcpClientService';
-import { useToast } from '../../contexts/ToastContext';
-import { DeleteConfirmModal } from '../common/DeleteConfirmModal';
-
-// Client interface (keeping for backward compatibility)
-export interface Client {
- id: string;
- name: string;
- status: 'online' | 'offline' | 'error';
- ip: string;
- lastSeen: string;
- version: string;
- tools: Tool[];
- region?: string;
- lastError?: string;
-}
-
-// Tool interface
-export interface Tool {
- id: string;
- name: string;
- description: string;
- parameters: ToolParameter[];
-}
-
-// Tool parameter interface
-export interface ToolParameter {
- name: string;
- type: 'string' | 'number' | 'boolean' | 'array';
- required: boolean;
- description?: string;
-}
-
-export const MCPClients = memo(() => {
- const [clients, setClients] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
-
- // State for selected client and panel visibility
- const [selectedClient, setSelectedClient] = useState(null);
- const [isPanelOpen, setIsPanelOpen] = useState(false);
- const [isAddClientModalOpen, setIsAddClientModalOpen] = useState(false);
-
- // State for edit drawer
- const [editClient, setEditClient] = useState(null);
- const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false);
-
- const { showToast } = useToast();
-
- // State for delete confirmation modal
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [clientToDelete, setClientToDelete] = useState(null);
-
- // Load clients when component mounts
- useEffect(() => {
- loadAllClients();
-
- // Set up periodic status checks every 10 seconds
- const statusInterval = setInterval(() => {
- // Silently refresh client statuses without loading state
- refreshClientStatuses();
- }, 10000);
-
- return () => clearInterval(statusInterval);
- }, []);
-
- /**
- * Refresh client statuses without showing loading state
- */
- const refreshClientStatuses = async () => {
- try {
- const dbClients = await mcpClientService.getClients();
-
- setClients(prevClients =>
- prevClients.map(client => {
- const dbClient = dbClients.find(db => db.id === client.id);
- if (dbClient) {
- return {
- ...client,
- status: dbClient.status === 'connected' ? 'online' :
- dbClient.status === 'error' ? 'error' : 'offline',
- lastSeen: dbClient.last_seen ? new Date(dbClient.last_seen).toLocaleString() : 'Never',
- lastError: dbClient.last_error || undefined
- };
- }
- return client;
- })
- );
- } catch (error) {
- console.warn('Failed to refresh client statuses:', error);
- }
- };
-
- /**
- * Load all clients: Archon (hardcoded) + real database clients
- */
- const loadAllClients = async () => {
- try {
- setIsLoading(true);
- setError(null);
-
- // Load ALL clients from database (including Archon)
- let dbClients: MCPClient[] = [];
- try {
- dbClients = await mcpClientService.getClients();
- } catch (clientError) {
- console.warn('Failed to load database clients:', clientError);
- dbClients = [];
- }
-
- // Convert database clients to our Client interface and load their tools
- const convertedClients: Client[] = await Promise.all(
- dbClients.map(async (dbClient) => {
- const client = convertDbClientToClient(dbClient);
- // Load tools for connected clients using universal method
- if (client.status === 'online') {
- await loadTools(client);
- }
- return client;
- })
- );
-
- // Set all clients (Archon will be included as a regular client)
- setClients(convertedClients);
- } catch (error) {
- console.error('Failed to load MCP clients:', error);
- setError(error instanceof Error ? error.message : 'Failed to load clients');
- setClients([]);
- } finally {
- setIsLoading(false);
- }
- };
-
- /**
- * Convert database MCP client to our Client interface
- */
- const convertDbClientToClient = (dbClient: MCPClient): Client => {
- // Map database status to our status types
- const statusMap: Record = {
- 'connected': 'online',
- 'disconnected': 'offline',
- 'connecting': 'offline',
- 'error': 'error'
- };
-
- // Extract connection info (Streamable HTTP-only)
- const config = dbClient.connection_config;
- const ip = config.url || 'N/A';
-
- return {
- id: dbClient.id,
- name: dbClient.name,
- status: statusMap[dbClient.status] || 'offline',
- ip,
- lastSeen: dbClient.last_seen ? new Date(dbClient.last_seen).toLocaleString() : 'Never',
- version: config.version || 'Unknown',
- region: config.region || 'Unknown',
- tools: [], // Will be loaded separately
- lastError: dbClient.last_error || undefined
- };
- };
-
- /**
- * Load tools from any MCP client using universal client service
- */
- const loadTools = async (client: Client) => {
- try {
- const toolsResponse = await mcpClientService.getClientTools(client.id);
-
- // Convert client tools to our Tool interface format
- const convertedTools: Tool[] = toolsResponse.tools.map((clientTool: any, index: number) => {
- const parameters: ToolParameter[] = [];
-
- // Extract parameters from tool schema
- if (clientTool.tool_schema?.inputSchema?.properties) {
- const required = clientTool.tool_schema.inputSchema.required || [];
- Object.entries(clientTool.tool_schema.inputSchema.properties).forEach(([name, schema]: [string, any]) => {
- parameters.push({
- name,
- type: schema.type === 'integer' ? 'number' :
- schema.type === 'array' ? 'array' :
- schema.type === 'boolean' ? 'boolean' : 'string',
- required: required.includes(name),
- description: schema.description || `${name} parameter`
- });
- });
- }
-
- return {
- id: `${client.id}-${index}`,
- name: clientTool.tool_name,
- description: clientTool.tool_description || 'No description available',
- parameters
- };
- });
-
- client.tools = convertedTools;
- console.log(`Loaded ${convertedTools.length} tools for client ${client.name}`);
- } catch (error) {
- console.error(`Failed to load tools for client ${client.name}:`, error);
- client.tools = [];
- }
- };
-
- /**
- * Handle adding a new client
- */
- const handleAddClient = async (clientConfig: MCPClientConfig) => {
- try {
- // Create client in database
- const newClient = await mcpClientService.createClient(clientConfig);
-
- // Convert and add to local state
- const convertedClient = convertDbClientToClient(newClient);
-
- // Try to load tools if client is connected
- if (convertedClient.status === 'online') {
- await loadTools(convertedClient);
- }
-
- setClients(prev => [...prev, convertedClient]);
-
- // Close modal
- setIsAddClientModalOpen(false);
-
- console.log('Client added successfully:', newClient.name);
- } catch (error) {
- console.error('Failed to add client:', error);
- setError(error instanceof Error ? error.message : 'Failed to add client');
- throw error; // Re-throw so modal can handle it
- }
- };
-
- // Handle client selection
- const handleSelectClient = async (client: Client) => {
- setSelectedClient(client);
- setIsPanelOpen(true);
-
- // Refresh tools for the selected client if needed
- if (client.tools.length === 0 && client.status === 'online') {
- await loadTools(client);
-
- // Update the client in the list
- setClients(prev => prev.map(c => c.id === client.id ? client : c));
- }
- };
-
- // Handle client editing
- const handleEditClient = (client: Client) => {
- setEditClient(client);
- setIsEditDrawerOpen(true);
- };
-
- // Handle client deletion (triggers confirmation modal)
- const handleDeleteClient = (client: Client) => {
- setClientToDelete(client);
- setShowDeleteConfirm(true);
- };
-
- // Refresh clients list (for after connection state changes)
- const refreshClients = async () => {
- try {
- const dbClients = await mcpClientService.getClients();
- const convertedClients = await Promise.all(
- dbClients.map(async (dbClient) => {
- const client = convertDbClientToClient(dbClient);
- if (client.status === 'online') {
- await loadTools(client);
- }
- return client;
- })
- );
- setClients(convertedClients);
- } catch (error) {
- console.error('Failed to refresh clients:', error);
- setError(error instanceof Error ? error.message : 'Failed to refresh clients');
- }
- };
-
- // Confirm deletion and execute
- const confirmDeleteClient = async () => {
- if (!clientToDelete) return;
-
- try {
- await mcpClientService.deleteClient(clientToDelete.id);
- setClients(prev => prev.filter(c => c.id !== clientToDelete.id));
- showToast(`MCP Client "${clientToDelete.name}" deleted successfully`, 'success');
- } catch (error) {
- console.error('Failed to delete MCP client:', error);
- showToast(error instanceof Error ? error.message : 'Failed to delete MCP client', 'error');
- } finally {
- setShowDeleteConfirm(false);
- setClientToDelete(null);
- }
- };
-
- // Cancel deletion
- const cancelDeleteClient = () => {
- setShowDeleteConfirm(false);
- setClientToDelete(null);
- };
-
- if (isLoading) {
- return (
-
-
-
-
Loading MCP clients...
-
-
- );
- }
-
- return (
-
- {/* Error display */}
- {error && (
-
-
{error}
-
setError(null)}
- className="text-red-500 hover:text-red-600 text-sm mt-2"
- >
- Dismiss
-
-
- )}
-
- {/* Add Client Button */}
-
-
-
MCP Clients
-
- Connect and manage your MCP-enabled applications
-
-
-
setIsAddClientModalOpen(true)}
- variant="primary"
- accentColor="cyan"
- className="shadow-cyan-500/20 shadow-sm"
- >
-
- Add Client
-
-
-
- {/* Client Grid */}
-
-
- {clients.map(client => (
- handleSelectClient(client)}
- onEdit={() => handleEditClient(client)}
- onDelete={() => handleDeleteClient(client)}
- onConnectionChange={refreshClients}
- />
- ))}
-
-
-
- {/* Tool Testing Panel */}
-
setIsPanelOpen(false)}
- />
-
- {/* Add Client Modal */}
- {isAddClientModalOpen && (
- setIsAddClientModalOpen(false)}
- onSubmit={handleAddClient}
- />
- )}
-
- {/* Edit Client Drawer */}
- {isEditDrawerOpen && editClient && (
- {
- setIsEditDrawerOpen(false);
- setEditClient(null);
- }}
- onUpdate={(updatedClient) => {
- // Update the client in state or remove if deleted
- setClients(prev => {
- if (!updatedClient) { // If updatedClient is null, it means deletion
- return prev.filter(c => c.id !== editClient?.id); // Remove the client that was being edited
- }
- return prev.map(c => c.id === updatedClient.id ? updatedClient : c);
- });
- setIsEditDrawerOpen(false);
- setEditClient(null);
- }}
- />
- )}
-
- {/* Delete Confirmation Modal for Clients */}
- {showDeleteConfirm && clientToDelete && (
-
- )}
-
- );
-});
-
-// Add Client Modal Component
-interface AddClientModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSubmit: (config: MCPClientConfig) => Promise;
-}
-
-const AddClientModal: React.FC = ({ isOpen, onClose, onSubmit }) => {
- const [formData, setFormData] = useState({
- name: '',
- url: '',
- auto_connect: true
- });
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [error, setError] = useState(null);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!formData.name.trim()) {
- setError('Client name is required');
- return;
- }
-
- setIsSubmitting(true);
- setError(null);
-
- try {
- // Validate URL
- if (!formData.url.trim()) {
- setError('MCP server URL is required');
- setIsSubmitting(false);
- return;
- }
-
- // Ensure URL is valid
- try {
- const url = new URL(formData.url);
- if (!url.protocol.startsWith('http')) {
- setError('URL must start with http:// or https://');
- setIsSubmitting(false);
- return;
- }
- } catch (e) {
- setError('Invalid URL format');
- setIsSubmitting(false);
- return;
- }
-
- const connection_config = {
- url: formData.url.trim()
- };
-
- const clientConfig: MCPClientConfig = {
- name: formData.name.trim(),
- transport_type: 'http',
- connection_config,
- auto_connect: formData.auto_connect
- };
-
- await onSubmit(clientConfig);
-
- // Reset form on success
- setFormData({
- name: '',
- url: '',
- auto_connect: true
- });
- setError(null);
- } catch (error) {
- setError(error instanceof Error ? error.message : 'Failed to add client');
- } finally {
- setIsSubmitting(false);
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
-
- Add New MCP Client
-
-
-
-
-
- );
-};
-
-// Edit Client Drawer Component
-interface EditClientDrawerProps {
- client: Client;
- isOpen: boolean;
- onClose: () => void;
- onUpdate: (client: Client | null) => void; // Allow null to indicate deletion
-}
-
-const EditClientDrawer: React.FC = ({ client, isOpen, onClose, onUpdate }) => {
- const [editFormData, setEditFormData] = useState({
- name: client.name,
- url: '',
- auto_connect: true
- });
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [error, setError] = useState(null);
- const [isConnecting, setIsConnecting] = useState(false);
-
- // State for delete confirmation modal (moved here)
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [clientToDelete, setClientToDelete] = useState(null);
-
- const { showToast } = useToast(); // Initialize useToast here
-
- // Load current client config when drawer opens
- useEffect(() => {
- if (isOpen && client) {
- // Get client config from the API and populate form
- loadClientConfig();
- }
- }, [isOpen, client.id]);
-
- const loadClientConfig = async () => {
- try {
- const dbClient = await mcpClientService.getClient(client.id);
- const config = dbClient.connection_config;
-
- setEditFormData({
- name: dbClient.name,
- url: config.url || '',
- auto_connect: dbClient.auto_connect
- });
- } catch (error) {
- console.error('Failed to load client config:', error);
- setError('Failed to load client configuration');
- }
- };
-
- const handleUpdateSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsSubmitting(true);
- setError(null);
-
- try {
- // Validate URL
- if (!editFormData.url.trim()) {
- setError('MCP server URL is required');
- setIsSubmitting(false);
- return;
- }
-
- // Ensure URL is valid
- try {
- const url = new URL(editFormData.url);
- if (!url.protocol.startsWith('http')) {
- setError('URL must start with http:// or https://');
- setIsSubmitting(false);
- return;
- }
- } catch (e) {
- setError('Invalid URL format');
- setIsSubmitting(false);
- return;
- }
-
- const connection_config = {
- url: editFormData.url.trim()
- };
-
- // Update client via API
- const updatedClient = await mcpClientService.updateClient(client.id, {
- name: editFormData.name,
- transport_type: 'http',
- connection_config,
- auto_connect: editFormData.auto_connect
- });
-
- // Update local state
- const convertedClient = {
- ...client,
- name: updatedClient.name,
- ip: editFormData.url
- };
-
- onUpdate(convertedClient);
- onClose();
- } catch (error) {
- setError(error instanceof Error ? error.message : 'Failed to update client');
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const handleConnect = async () => {
- setIsConnecting(true);
- try {
- await mcpClientService.connectClient(client.id);
- // Reload the client to get updated status
- loadClientConfig();
- } catch (error) {
- setError(error instanceof Error ? error.message : 'Failed to connect');
- } finally {
- setIsConnecting(false);
- }
- };
-
- const handleDisconnect = async () => {
- try {
- await mcpClientService.disconnectClient(client.id);
- // Reload the client to get updated status
- loadClientConfig();
- } catch (error) {
- setError(error instanceof Error ? error.message : 'Failed to disconnect');
- }
- };
-
- const handleDelete = async () => {
- if (confirm(`Are you sure you want to delete "${client.name}"?`)) {
- try {
- await mcpClientService.deleteClient(client.id);
- onClose();
- // Trigger a reload of the clients list
- window.location.reload();
- } catch (error) {
- setError(error instanceof Error ? error.message : 'Failed to delete client');
- }
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
e.stopPropagation()}
- >
-
-
-
-
- Edit Client Configuration
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/mcp/ToolTestingPanel.tsx b/archon-ui-main/src/components/mcp/ToolTestingPanel.tsx
deleted file mode 100644
index f1866c8440..0000000000
--- a/archon-ui-main/src/components/mcp/ToolTestingPanel.tsx
+++ /dev/null
@@ -1,568 +0,0 @@
-import React, { useEffect, useState, useRef } from 'react';
-import { X, Play, ChevronDown, TerminalSquare, Copy, Check, MinusCircle, Maximize2, Minimize2, Hammer, GripHorizontal } from 'lucide-react';
-import { Client, Tool } from './MCPClients';
-import { Button } from '../ui/Button';
-import { mcpClientService } from '../../services/mcpClientService';
-
-interface ToolTestingPanelProps {
- client: Client | null;
- isOpen: boolean;
- onClose: () => void;
-}
-
-interface TerminalLine {
- id: string;
- content: string;
- isTyping: boolean;
- isCommand: boolean;
- isError?: boolean;
- isWarning?: boolean;
-}
-
-export const ToolTestingPanel = ({
- client,
- isOpen,
- onClose
-}: ToolTestingPanelProps) => {
- const [selectedTool, setSelectedTool] = useState(null);
- const [terminalOutput, setTerminalOutput] = useState([{
- id: '1',
- content: '> Tool testing terminal ready',
- isTyping: false,
- isCommand: true
- }]);
- const [paramValues, setParamValues] = useState>({});
- const [isCopied, setIsCopied] = useState(false);
- const [panelHeight, setPanelHeight] = useState(400);
- const [isResizing, setIsResizing] = useState(false);
- const [isMaximized, setIsMaximized] = useState(false);
- const [isExecuting, setIsExecuting] = useState(false);
- const terminalRef = useRef(null);
- const resizeHandleRef = useRef(null);
- const panelRef = useRef(null);
- const previousHeightRef = useRef(400);
-
- // Reset selected tool when client changes
- useEffect(() => {
- if (client && client.tools.length > 0) {
- setSelectedTool(client.tools[0]);
- setParamValues({});
- } else {
- setSelectedTool(null);
- setParamValues({});
- }
- }, [client]);
-
- // Auto-scroll terminal to bottom when output changes
- useEffect(() => {
- if (terminalRef.current) {
- terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
- }
- }, [terminalOutput]);
-
- // Handle resizing functionality
- useEffect(() => {
- const handleMouseMove = (e: MouseEvent) => {
- if (isResizing && panelRef.current) {
- const containerHeight = window.innerHeight;
- const mouseY = e.clientY;
- const newHeight = containerHeight - mouseY;
- if (newHeight >= 200 && newHeight <= containerHeight * 0.8) {
- setPanelHeight(newHeight);
- }
- }
- };
-
- const handleMouseUp = () => {
- setIsResizing(false);
- document.body.style.cursor = 'default';
- document.body.style.userSelect = 'auto';
- };
-
- if (isResizing) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = 'ns-resize';
- document.body.style.userSelect = 'none';
- }
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- };
- }, [isResizing]);
-
- // Handle tool selection
- const handleToolSelect = (tool: Tool) => {
- setSelectedTool(tool);
- setParamValues({});
- };
-
- // Handle parameter value change
- const handleParamChange = (paramName: string, value: string) => {
- setParamValues(prev => ({
- ...prev,
- [paramName]: value
- }));
- };
-
- // Simulate typing animation for terminal output
- const addTypingLine = (content: string, isCommand: boolean = false, isError: boolean = false, isWarning: boolean = false) => {
- const newLineId = Date.now().toString() + Math.random().toString(36).substring(2);
-
- setTerminalOutput(prev => [...prev, {
- id: newLineId,
- content: '',
- isTyping: true,
- isCommand,
- isError,
- isWarning
- }]);
-
- // Simulate typing animation
- let currentText = '';
- const textArray = content.split('');
- const typeInterval = setInterval(() => {
- if (textArray.length > 0) {
- currentText += textArray.shift();
- setTerminalOutput(prev => prev.map(line =>
- line.id === newLineId ? {
- ...line,
- content: currentText
- } : line
- ));
- } else {
- clearInterval(typeInterval);
- setTerminalOutput(prev => prev.map(line =>
- line.id === newLineId ? {
- ...line,
- isTyping: false
- } : line
- ));
- }
- }, 15); // Faster typing
-
- return newLineId;
- };
-
- // Add instant line (no typing effect)
- const addInstantLine = (content: string, isCommand: boolean = false, isError: boolean = false, isWarning: boolean = false) => {
- const newLineId = Date.now().toString() + Math.random().toString(36).substring(2);
-
- setTerminalOutput(prev => [...prev, {
- id: newLineId,
- content,
- isTyping: false,
- isCommand,
- isError,
- isWarning
- }]);
-
- return newLineId;
- };
-
- // Convert parameter values to proper types
- const convertParameterValues = (): Record => {
- if (!selectedTool) return {};
-
- const convertedParams: Record = {};
-
- selectedTool.parameters.forEach(param => {
- const value = paramValues[param.name];
-
- if (value !== undefined && value !== '') {
- try {
- switch (param.type) {
- case 'number':
- convertedParams[param.name] = Number(value);
- if (isNaN(convertedParams[param.name])) {
- throw new Error(`Invalid number: ${value}`);
- }
- break;
- case 'boolean':
- convertedParams[param.name] = value.toLowerCase() === 'true' || value === '1';
- break;
- case 'array':
- // Try to parse as JSON array first, fallback to comma-separated
- try {
- convertedParams[param.name] = JSON.parse(value);
- if (!Array.isArray(convertedParams[param.name])) {
- throw new Error('Not an array');
- }
- } catch {
- convertedParams[param.name] = value.split(',').map(v => v.trim()).filter(v => v);
- }
- break;
- default:
- convertedParams[param.name] = value;
- }
- } catch (error) {
- console.warn(`Parameter conversion error for ${param.name}:`, error);
- convertedParams[param.name] = value; // Fallback to string
- }
- }
- });
-
- return convertedParams;
- };
-
-
-
- // Execute tool using universal MCP client service (works for ALL clients)
- const executeTool = async () => {
- if (!selectedTool || !client) return;
-
- try {
- const convertedParams = convertParameterValues();
-
- addTypingLine(`> Connecting to ${client.name} via MCP protocol...`);
-
- // Call the client tool via MCP service
- const result = await mcpClientService.callClientTool({
- client_id: client.id,
- tool_name: selectedTool.name,
- arguments: convertedParams
- });
-
- setTimeout(() => addTypingLine('> Tool executed successfully'), 300);
-
- // Display the result
- setTimeout(() => {
- if (result) {
- let resultText = '';
-
- if (typeof result === 'object') {
- if (result.content) {
- // Handle MCP content response
- if (Array.isArray(result.content)) {
- resultText = result.content.map((item: any) =>
- item.text || JSON.stringify(item, null, 2)
- ).join('\n');
- } else {
- resultText = result.content.text || JSON.stringify(result.content, null, 2);
- }
- } else {
- resultText = JSON.stringify(result, null, 2);
- }
- } else {
- resultText = String(result);
- }
-
- addInstantLine('> Result:');
- addInstantLine(resultText);
- } else {
- addTypingLine('> No result returned');
- }
-
- addTypingLine('> Completed successfully');
- setIsExecuting(false);
- }, 600);
-
- } catch (error: any) {
- console.error('MCP tool execution failed:', error);
- setTimeout(() => {
- addTypingLine(`> ERROR: Failed to execute tool on ${client.name}`, false, true);
- addTypingLine(`> ${error.message || 'Unknown error occurred'}`, false, true);
- addTypingLine('> Execution failed');
- setIsExecuting(false);
- }, 300);
- }
- };
-
- // Validate required parameters
- const validateParameters = (): string | null => {
- if (!selectedTool) return 'No tool selected';
-
- for (const param of selectedTool.parameters) {
- if (param.required && !paramValues[param.name]) {
- return `Required parameter '${param.name}' is missing`;
- }
- }
-
- return null;
- };
-
- // Handle tool execution
- const executeSelectedTool = () => {
- if (!selectedTool || !client || isExecuting) return;
-
- // Validate required parameters
- const validationError = validateParameters();
- if (validationError) {
- addTypingLine(`> ERROR: ${validationError}`, false, true);
- return;
- }
-
- setIsExecuting(true);
-
- // Add command to terminal
- const params = selectedTool.parameters.map(p => {
- const value = paramValues[p.name];
- return value ? `${p.name}=${value}` : undefined;
- }).filter(Boolean).join(' ');
-
- const command = `> execute ${selectedTool.name} ${params}`;
- addTypingLine(command, true);
-
- // Execute using universal client service for ALL clients
- setTimeout(() => {
- executeTool();
- }, 200);
- };
-
- // Handle copy terminal output
- const copyTerminalOutput = () => {
- const textContent = terminalOutput.map(line => line.content).join('\n');
- navigator.clipboard.writeText(textContent);
- setIsCopied(true);
- setTimeout(() => setIsCopied(false), 2000);
- };
-
- // Handle resize start
- const handleResizeStart = (e: React.MouseEvent) => {
- e.preventDefault();
- setIsResizing(true);
- };
-
- // Handle maximize/minimize
- const toggleMaximize = () => {
- if (isMaximized) {
- setPanelHeight(previousHeightRef.current);
- } else {
- previousHeightRef.current = panelHeight;
- setPanelHeight(window.innerHeight * 0.8);
- }
- setIsMaximized(!isMaximized);
- };
-
- // Clear terminal
- const clearTerminal = () => {
- setTerminalOutput([{
- id: Date.now().toString(),
- content: '> Terminal cleared',
- isTyping: false,
- isCommand: true
- }]);
- };
-
- if (!isOpen || !client) return null;
-
- return (
-
- {/* Resize handle at the top */}
-
-
- {/* Panel with neon effect */}
-
-
-
- {/* Header */}
-
-
-
- {client.name}
-
- {client.ip}
-
-
- {client.tools.length} tools available
-
-
-
-
-
-
-
- {isMaximized ? : }
-
-
-
-
-
-
-
- {/* Content */}
-
- {client.tools.length === 0 ? (
-
-
-
-
No Tools Available
-
- {client.status === 'offline'
- ? 'Client is offline. Tools will be available when connected.'
- : 'No tools discovered for this client.'}
-
-
-
- ) : (
-
- {/* Left column: Tool selection and parameters */}
-
- {/* Tool selection and execute button row */}
-
-
-
- Select Tool
-
-
-
{
- const tool = client.tools.find(t => t.id === e.target.value);
- if (tool) handleToolSelect(tool);
- }}
- >
- {client.tools.map(tool => (
-
- {tool.name}
-
- ))}
-
-
-
-
-
-
-
-
- {isExecuting ? (
-
-
- Executing...
-
- ) : (
- <>
-
- Execute Tool
- >
- )}
-
-
-
-
- {/* Tool description */}
- {selectedTool && (
-
- {selectedTool.description}
-
- )}
-
- {/* Parameters */}
- {selectedTool && selectedTool.parameters.length > 0 && (
-
-
- Parameters
-
-
- {selectedTool.parameters.map(param => (
-
-
- {param.name}
- {param.required && * }
- ({param.type})
-
-
handleParamChange(param.name, e.target.value)}
- className="w-full px-3 py-2 text-sm bg-white/50 dark:bg-black/50 border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-1 focus:ring-cyan-500 focus:border-cyan-500 transition-all duration-200"
- placeholder={param.description || `Enter ${param.name}`}
- />
- {param.description && (
-
- {param.description}
-
- )}
-
- ))}
-
-
- )}
-
-
- {/* Right column: Terminal output */}
-
-
-
-
-
-
- Terminal Output
-
-
-
- {isCopied ?
- :
-
- }
-
-
-
- {terminalOutput.map(line => (
-
- {line.content}
- {line.isTyping && ▌ }
-
- ))}
-
-
-
-
- )}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/DataTab.tsx b/archon-ui-main/src/components/project-tasks/DataTab.tsx
deleted file mode 100644
index 6ba8dac2d6..0000000000
--- a/archon-ui-main/src/components/project-tasks/DataTab.tsx
+++ /dev/null
@@ -1,956 +0,0 @@
-import React, { useCallback, useState, useEffect, useMemo } from 'react';
-import '@xyflow/react/dist/style.css';
-import { ReactFlow, Node, Edge, Background, Controls, MarkerType, NodeChange, applyNodeChanges, EdgeChange, applyEdgeChanges, ConnectionLineType, addEdge, Connection, Handle, Position } from '@xyflow/react';
-import { Database, Info, Calendar, TrendingUp, Edit, Plus, X, Save, Trash2 } from 'lucide-react';
-import { projectService } from '../../services/projectService';
-import { useToast } from '../../contexts/ToastContext';
-
-// Custom node types - will be defined inside the component to access state
-
-const createTableNode = (id: string, label: string, columns: string[], x: number, y: number): Node => ({
- id,
- type: 'table',
- data: {
- label,
- columns
- },
- position: {
- x,
- y
- }
-});
-
-// Default fallback nodes for basic database structure
-const defaultNodes: Node[] = [
- createTableNode('users', 'Users', ['id (PK) - UUID', 'email - VARCHAR(255)', 'password - VARCHAR(255)', 'firstName - VARCHAR(100)', 'lastName - VARCHAR(100)', 'createdAt - TIMESTAMP', 'updatedAt - TIMESTAMP'], 150, 100),
- createTableNode('projects', 'Projects', ['id (PK) - UUID', 'title - VARCHAR(255)', 'description - TEXT', 'status - VARCHAR(50)', 'userId (FK) - UUID', 'createdAt - TIMESTAMP', 'updatedAt - TIMESTAMP'], 500, 100)
-];
-
-const defaultEdges: Edge[] = [{
- id: 'projects-users',
- source: 'users',
- target: 'projects',
- sourceHandle: 'Users-id',
- targetHandle: 'Projects-userId',
- animated: true,
- style: {
- stroke: '#d946ef'
- },
- markerEnd: {
- type: MarkerType.Arrow,
- color: '#d946ef'
- }
-}];
-
-// Data metadata card component for the new data structure
-const DataCard = ({ data }: { data: any }) => {
- const iconMap: { [key: string]: any } = {
- 'ShoppingCart': Database,
- 'Database': Database,
- 'Info': Info,
- 'Calendar': Calendar,
- 'TrendingUp': TrendingUp
- };
-
- const IconComponent = iconMap[data.icon] || Database;
-
- const colorClasses = {
- cyan: 'from-cyan-900/40 to-cyan-800/30 border-cyan-500/50 text-cyan-400',
- blue: 'from-blue-900/40 to-blue-800/30 border-blue-500/50 text-blue-400',
- purple: 'from-purple-900/40 to-purple-800/30 border-purple-500/50 text-purple-400',
- pink: 'from-pink-900/40 to-pink-800/30 border-pink-500/50 text-pink-400'
- };
-
- const colorClass = colorClasses[data.color as keyof typeof colorClasses] || colorClasses.cyan;
-
- return (
-
-
-
-
Project Data Overview
-
-
-
-
Description:
-
{data.description}
-
-
-
- Last updated: {data.updated}
-
-
-
- );
-};
-
-interface DataTabProps {
- project?: {
- id: string;
- title: string;
- data?: any[];
- } | null;
-}
-
-export const DataTab = ({ project }: DataTabProps) => {
- const [nodes, setNodes] = useState([]);
- const [edges, setEdges] = useState([]);
- const [loading, setLoading] = useState(true);
- const [viewMode, setViewMode] = useState<'metadata' | 'erd'>('metadata');
- const [editingNode, setEditingNode] = useState(null);
- const [showEditModal, setShowEditModal] = useState(false);
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
- const [isSaving, setIsSaving] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [nodeToDelete, setNodeToDelete] = useState(null);
-
- const { showToast } = useToast();
-
-
- // Helper function to normalize nodes to ensure required properties
- const normalizeNode = (node: any): Node => {
- return {
- id: node.id || `node-${Date.now()}-${Math.random()}`,
- type: node.type || 'table',
- position: {
- x: node.position?.x || 100,
- y: node.position?.y || 100
- },
- data: {
- label: node.data?.label || 'Untitled',
- columns: node.data?.columns || ['id (PK) - UUID']
- }
- };
- };
-
- useEffect(() => {
- console.log('DataTab project data:', project?.data);
-
- // Determine view mode based on data structure
- if (project?.data) {
- if (Array.isArray(project.data) && project.data.length > 0) {
- // Handle array format: [{"type": "erd", ...}] or [{"description": "...", "progress": 65}]
- const firstItem = project.data[0];
- console.log('First data item (array):', firstItem);
-
- if (firstItem.description && typeof firstItem.progress === 'number') {
- console.log('Setting metadata view');
- setViewMode('metadata');
- } else if (firstItem.type === 'erd' && firstItem.nodes && firstItem.edges) {
- console.log('Setting ERD view with structured array data');
- setViewMode('erd');
- // Normalize nodes to ensure required properties
- const normalizedNodes = firstItem.nodes.map(normalizeNode);
- setNodes(normalizedNodes);
- // Fix any ArrowClosed marker types in loaded edges
- const sanitizedEdges = firstItem.edges.map((edge: any) => ({
- ...edge,
- markerEnd: edge.markerEnd ? {
- ...edge.markerEnd,
- type: edge.markerEnd.type === 'ArrowClosed' ? MarkerType.Arrow : edge.markerEnd.type
- } : undefined
- }));
- setEdges(sanitizedEdges);
- } else {
- console.log('Setting ERD view for array data');
- setViewMode('erd');
- // Normalize nodes to ensure required properties
- const normalizedNodes = project.data.map(normalizeNode);
- setNodes(normalizedNodes);
- setEdges([]);
- }
- } else if (typeof project.data === 'object' && !Array.isArray(project.data) &&
- (project.data as any).type === 'erd' &&
- (project.data as any).nodes &&
- (project.data as any).edges) {
- // Handle direct object format: {"type": "erd", "nodes": [...], "edges": [...]}
- console.log('Setting ERD view with direct object data');
- setViewMode('erd');
- // Normalize nodes to ensure required properties
- const normalizedNodes = (project.data as any).nodes.map(normalizeNode);
- setNodes(normalizedNodes);
- // Fix any ArrowClosed marker types in loaded edges
- const sanitizedEdges = (project.data as any).edges.map((edge: any) => ({
- ...edge,
- markerEnd: edge.markerEnd ? {
- ...edge.markerEnd,
- type: edge.markerEnd.type === 'ArrowClosed' ? MarkerType.Arrow : edge.markerEnd.type
- } : undefined
- }));
- setEdges(sanitizedEdges);
- } else {
- console.log('Unknown data format, showing empty state');
- setViewMode('erd');
- setNodes([]);
- setEdges([]);
- }
- } else {
- console.log('No data, using empty state');
- setViewMode('erd');
- setNodes([]);
- setEdges([]);
- }
- setLoading(false);
- }, [project]);
-
- const onNodesChange = useCallback((changes: NodeChange[]) => {
- setNodes(nds => applyNodeChanges(changes, nds));
- setHasUnsavedChanges(true);
- }, []);
-
- const onEdgesChange = useCallback((changes: EdgeChange[]) => {
- setEdges(eds => applyEdgeChanges(changes, eds));
- setHasUnsavedChanges(true);
- }, []);
- const onConnect = useCallback(async (connection: Connection) => {
- const newEdgeProps = {
- animated: true,
- style: {
- stroke: '#22d3ee'
- },
- markerEnd: {
- type: MarkerType.Arrow,
- color: '#22d3ee'
- },
- label: 'relates to',
- labelStyle: {
- fill: '#e94560',
- fontWeight: 500
- },
- labelBgStyle: {
- fill: 'rgba(0, 0, 0, 0.7)'
- }
- };
-
- const newEdges = addEdge({ ...connection, ...newEdgeProps }, edges);
- setEdges(newEdges);
-
- // Auto-save to database
- await saveToDatabase(nodes, newEdges);
- }, [nodes, edges, project?.id]);
-
- const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
- setEditingNode(node);
- setShowEditModal(true);
- }, []);
-
-
-
- const addTableNode = async () => {
- if (!project?.id) {
- console.error('❌ No project ID available for adding table');
- return;
- }
-
- console.log('🔄 Adding new table...');
- const newNodeId = `table-${Date.now()}`;
- const newNode = createTableNode(newNodeId, `New Table ${nodes.length + 1}`, ['id (PK) - UUID', 'name - VARCHAR(255)', 'description - TEXT', 'createdAt - TIMESTAMP', 'updatedAt - TIMESTAMP'], 400, 300);
- const newNodes = [...nodes, newNode];
- setNodes(newNodes);
-
- // Auto-save to database
- try {
- console.log('💾 Saving new table to database...');
- await saveToDatabase(newNodes, edges);
- console.log('✅ New table saved successfully');
- } catch (error) {
- console.error('❌ Failed to save new table:', error);
- // Optionally revert the UI change if save failed
- setNodes(nodes);
- }
- };
-
- const saveToDatabase = async (nodesToSave = nodes, edgesToSave = edges) => {
- if (!project?.id) {
- console.error('No project ID available for saving');
- return;
- }
-
- console.log('💾 saveToDatabase called with:', {
- projectId: project.id,
- nodeCount: nodesToSave.length,
- edgeCount: edgesToSave.length
- });
-
- setIsSaving(true);
- try {
- const updatedData = {
- type: 'erd',
- nodes: nodesToSave,
- edges: edgesToSave
- };
-
- console.log('🔄 Calling projectService.updateProject with data:', updatedData);
-
- const result = await projectService.updateProject(project.id, {
- data: [updatedData] // Wrap in array to match UpdateProjectRequest type
- });
-
- console.log('✅ ERD data saved successfully, result:', result);
- setHasUnsavedChanges(false);
- } catch (error) {
- console.error('❌ Failed to save ERD data:', error);
- console.error('Error details:', error);
- throw error; // Re-throw so calling function can handle it
- } finally {
- setIsSaving(false);
- }
- };
-
- const saveNodeChanges = async (updatedNode: Node) => {
- // Update local state first
- const newNodes = nodes.map(node =>
- node.id === updatedNode.id ? updatedNode : node
- );
- setNodes(newNodes);
-
- // Save to database
- await saveToDatabase(newNodes, edges);
-
- setShowEditModal(false);
- setEditingNode(null);
- };
-
- const handleManualSave = async () => {
- await saveToDatabase();
- };
-
- const handleDeleteNode = useCallback(async (event: React.MouseEvent, nodeId: string) => {
- event.stopPropagation(); // Prevent triggering the edit modal
-
- if (!project?.id) {
- console.error('❌ No project ID available for deleting table');
- return;
- }
-
- // Show custom confirmation dialog
- setNodeToDelete(nodeId);
- setShowDeleteConfirm(true);
- }, [project?.id]);
-
- const confirmDelete = useCallback(async () => {
- if (!nodeToDelete) return;
-
- console.log('🗑️ Deleting table:', nodeToDelete);
-
- try {
- // Remove node from UI
- const newNodes = nodes.filter(node => node.id !== nodeToDelete);
-
- // Remove any edges connected to this node
- const newEdges = edges.filter(edge =>
- edge.source !== nodeToDelete && edge.target !== nodeToDelete
- );
-
- setNodes(newNodes);
- setEdges(newEdges);
-
- // Save to database
- console.log('💾 Saving after table deletion...');
- await saveToDatabase(newNodes, newEdges);
- console.log('✅ Table deleted successfully');
- showToast('Table deleted successfully', 'success');
-
- // Close confirmation dialog
- setShowDeleteConfirm(false);
- setNodeToDelete(null);
- } catch (error) {
- console.error('❌ Failed to delete table:', error);
- // Revert UI changes on error
- setNodes(nodes);
- setEdges(edges);
- showToast('Failed to delete table', 'error');
- }
- }, [nodeToDelete, nodes, edges, saveToDatabase]);
-
- const cancelDelete = useCallback(() => {
- setShowDeleteConfirm(false);
- setNodeToDelete(null);
- }, []);
-
- // Memoize nodeTypes to prevent recreation on every render
- const nodeTypes = useMemo(() => ({
- table: ({ data, id }: any) => (
-
-
-
-
- handleDeleteNode(e, id)}
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-600/20 rounded"
- title="Delete table"
- >
-
-
- {
- e.stopPropagation();
- // Find the actual node from the nodes array instead of creating a fake one
- const actualNode = nodes.find(node => node.id === id);
- if (actualNode) {
- handleNodeClick(e, actualNode);
- }
- }}
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-cyan-600/20 rounded"
- title="Edit table"
- >
-
-
-
-
-
- {data.columns.map((col: string, i: number) => {
- const isPK = col.includes('PK');
- const isFK = col.includes('FK');
- return (
-
- {col}
- {isPK && (
-
- )}
- {isFK && (
-
- )}
-
- );
- })}
-
-
- )
- }), [handleNodeClick, handleDeleteNode, nodes]);
-
- if (loading) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
-
- {viewMode === 'metadata' ? 'Data Overview' : 'Data Relationships'}
- {viewMode === 'erd' && nodes.length > 0 && ` (${nodes.length} tables)`}
- {viewMode === 'metadata' && Array.isArray(project?.data) && ` (${project.data.length} items)`}
-
- {viewMode === 'metadata' && (
-
setViewMode('erd')}
- className="px-3 py-1.5 rounded-lg bg-cyan-900/20 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-900/30 hover:border-cyan-500/50 transition-all duration-300 text-xs"
- >
- Switch to ERD
-
- )}
- {viewMode === 'erd' && (
-
- setViewMode('metadata')}
- className="px-3 py-1.5 rounded-lg bg-purple-900/20 border border-purple-500/30 text-purple-400 hover:bg-purple-900/30 hover:border-purple-500/50 transition-all duration-300 text-xs"
- >
- Data Overview
-
- {hasUnsavedChanges && (
-
-
- {isSaving ? 'Saving...' : 'Save Layout'}
-
- )}
-
-
-
- Add Table
-
-
- )}
-
-
- {viewMode === 'metadata' ? (
-
- {Array.isArray(project?.data) && project.data.length > 0 ? (
-
- {project.data.map((item, index) => (
-
- ))}
-
- ) : (
-
-
-
No metadata available
-
Switch to ERD view to see database schema
-
- )}
-
- ) : (
-
- {/* Subtle neon glow at the top */}
-
- {nodes.length === 0 ? (
-
-
-
No data schema defined
-
Add tables to design your database
-
- ) : (
-
-
-
- )}
-
- )}
-
- {/* Delete Confirmation Modal */}
- {showDeleteConfirm && (
-
n.id === nodeToDelete)?.data.label as string || 'table'}
- />
- )}
-
- {/* Edit Modal */}
- {showEditModal && editingNode && (
- {
- setShowEditModal(false);
- setEditingNode(null);
- }}
- />
- )}
-
-
- );
-};
-
-// Delete Confirmation Modal Component
-const DeleteConfirmModal = ({
- onConfirm,
- onCancel,
- tableName
-}: {
- onConfirm: () => void;
- onCancel: () => void;
- tableName: string;
-}) => {
- return (
-
-
-
-
-
-
-
-
-
-
- Delete Table
-
-
- This action cannot be undone
-
-
-
-
-
- Are you sure you want to delete the "{tableName}" table?
- This will also remove all related connections.
-
-
-
-
- Cancel
-
-
- Delete Table
-
-
-
-
-
- );
-};
-
-// Column interface for better type management
-interface ColumnDefinition {
- name: string;
- dataType: string;
- columnType: 'regular' | 'pk' | 'fk';
- referencedTable?: string;
- referencedColumn?: string;
-}
-
-// Edit Table Modal Component
-const EditTableModal = ({
- node,
- nodes,
- edges,
- onSave,
- onUpdateEdges,
- onClose
-}: {
- node: Node;
- nodes: Node[];
- edges: Edge[];
- onSave: (node: Node) => void;
- onUpdateEdges: (edges: Edge[]) => void;
- onClose: () => void;
-}) => {
- const [tableName, setTableName] = useState(node.data.label as string);
- const [columns, setColumns] = useState([]);
-
- // Parse existing columns into structured format
- useEffect(() => {
- const parsedColumns = (node.data.columns as string[]).map((colStr: string) => {
- const parts = colStr.split(' - ');
- const nameAndType = parts[0];
- const dataType = parts[1] || 'VARCHAR(255)';
-
- let columnType: 'regular' | 'pk' | 'fk' = 'regular';
- let name = nameAndType;
- const referencedTable = '';
- const referencedColumn = '';
-
- if (nameAndType.includes('(PK)')) {
- columnType = 'pk';
- name = nameAndType.replace(' (PK)', '');
- } else if (nameAndType.includes('(FK)')) {
- columnType = 'fk';
- name = nameAndType.replace(' (FK)', '');
- }
-
- return {
- name,
- dataType,
- columnType,
- referencedTable,
- referencedColumn
- };
- });
- setColumns(parsedColumns);
- }, [node.data.columns]);
-
- const addColumn = () => {
- setColumns([...columns, {
- name: 'newColumn',
- dataType: 'VARCHAR(255)',
- columnType: 'regular',
- referencedTable: '',
- referencedColumn: ''
- }]);
- };
-
- const updateColumn = (index: number, field: keyof ColumnDefinition, value: string) => {
- const newColumns = [...columns];
- newColumns[index] = { ...newColumns[index], [field]: value };
- setColumns(newColumns);
- };
-
- const removeColumn = (index: number) => {
- setColumns(columns.filter((_, i) => i !== index));
- };
-
- // Get available tables for FK references (exclude current table)
- const getAvailableTables = () => {
- return nodes.filter(n => n.id !== node.id).map(n => ({
- id: n.id,
- label: n.data.label as string
- }));
- };
-
- // Get available columns for a specific table
- const getAvailableColumns = (tableId: string) => {
- const targetNode = nodes.find(n => n.id === tableId);
- if (!targetNode) return [];
-
- return (targetNode.data.columns as string[])
- .filter(col => col.includes('(PK)')) // Only allow referencing primary keys
- .map(col => {
- const name = col.split(' - ')[0].replace(' (PK)', '');
- return { name, label: name };
- });
- };
-
- const handleSave = () => {
- // Convert columns back to string format
- const columnStrings = columns.map(col => {
- let name = col.name;
- if (col.columnType === 'pk') {
- name += ' (PK)';
- } else if (col.columnType === 'fk') {
- name += ' (FK)';
- }
- return `${name} - ${col.dataType}`;
- });
-
- const updatedNode = {
- ...node,
- data: {
- ...node.data,
- label: tableName,
- columns: columnStrings
- }
- };
-
- // Create edges for FK relationships
- const newEdges = [...edges];
-
- // Remove existing edges from this table
- const filteredEdges = newEdges.filter(edge => edge.source !== node.id);
-
- // Add new edges for FK columns
- columns.forEach(col => {
- if (col.columnType === 'fk' && col.referencedTable && col.referencedColumn) {
- const edgeId = `${col.referencedTable}-${node.id}`;
- const newEdge = {
- id: edgeId,
- source: col.referencedTable,
- target: node.id,
- sourceHandle: `${nodes.find(n => n.id === col.referencedTable)?.data.label}-${col.referencedColumn}`,
- targetHandle: `${tableName}-${col.name}`,
- animated: true,
- style: {
- stroke: '#d946ef'
- },
- markerEnd: {
- type: MarkerType.Arrow,
- color: '#d946ef'
- }
- };
- filteredEdges.push(newEdge);
- }
- });
-
- // Update edges state
- onUpdateEdges(filteredEdges);
-
- onSave(updatedNode);
- };
-
- return (
-
-
-
-
-
- Edit Table
-
-
-
-
-
-
-
- {/* Table Name */}
-
-
- Table Name
-
- setTableName(e.target.value)}
- className="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-cyan-500 focus:outline-none"
- />
-
-
- {/* Columns */}
-
-
-
- Columns
-
-
-
- Add Column
-
-
-
- {/* Column Headers */}
-
-
Column Name
-
Data Type
-
Type
-
References (FK only)
-
-
-
-
- {columns.map((column, index) => (
-
- {/* Column Name */}
-
updateColumn(index, 'name', e.target.value)}
- className="col-span-3 px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-cyan-500 focus:outline-none text-sm"
- />
-
- {/* Data Type */}
-
updateColumn(index, 'dataType', e.target.value)}
- className="col-span-2 px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-cyan-500 focus:outline-none text-sm"
- />
-
- {/* Column Type */}
-
updateColumn(index, 'columnType', e.target.value)}
- className="col-span-2 px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-cyan-500 focus:outline-none text-sm"
- >
- Regular
- Primary Key
- Foreign Key
-
-
- {/* FK Reference (only show for FK columns) */}
- {column.columnType === 'fk' && (
- <>
-
updateColumn(index, 'referencedTable', e.target.value)}
- className="col-span-2 px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-cyan-500 focus:outline-none text-sm"
- >
- Select table...
- {getAvailableTables().map((table) => (
-
- {table.label}
-
- ))}
-
-
-
updateColumn(index, 'referencedColumn', e.target.value)}
- disabled={!column.referencedTable}
- className="col-span-2 px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-cyan-500 focus:outline-none text-sm disabled:opacity-50"
- >
- Select column...
- {column.referencedTable && getAvailableColumns(column.referencedTable).map((col) => (
-
- {col.label}
-
- ))}
-
- >
- )}
-
- {/* Spacer for non-FK columns */}
- {column.columnType !== 'fk' &&
}
-
- {/* Remove Button */}
-
removeColumn(index)}
- className="col-span-1 flex items-center justify-center p-1 text-red-400 hover:text-red-300 hover:bg-red-600/10 rounded transition-colors"
- title="Delete column"
- >
-
-
-
- ))}
-
-
-
-
- {/* Actions */}
-
-
- Save Changes
-
-
- Cancel
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/DocsTab.tsx b/archon-ui-main/src/components/project-tasks/DocsTab.tsx
deleted file mode 100644
index e2e0862425..0000000000
--- a/archon-ui-main/src/components/project-tasks/DocsTab.tsx
+++ /dev/null
@@ -1,1494 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Plus, X, Search, Upload, Link as LinkIcon, Check, Brain, Save, History, Eye, Edit3, Sparkles } 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 { useCrawlProgressPolling } from '../../hooks/usePolling';
-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;
-}
-
-interface Task {
- id: string;
- title: string;
- feature: string;
- status: 'backlog' | 'in-progress' | 'review' | 'complete';
-}
-
-// 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: 'HTTP polling for live updates',
- patterns: 'ETag-based polling with smart pausing'
- },
- 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 → HTTP polling → 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
-}: {
- tasks: Task[];
- project?: {
- id: string;
- title: string;
- created_at?: string;
- updated_at?: string;
- } | null;
-}) => {
- // 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 [activeProgressId, setActiveProgressId] = useState(null);
- const { showToast } = useToast();
-
- // Poll for crawl progress
- const crawlProgress = useCrawlProgressPolling(activeProgressId);
-
- // Load project documents from the project data
- const loadProjectDocuments = async () => {
- if (!project?.id || !project.docs) return;
-
- try {
- setLoading(true);
-
- // Use the docs directly from the project data
- const projectDocuments: ProjectDoc[] = project.docs.map((doc: any) => ({
- id: doc.id,
- title: doc.title || 'Untitled Document',
- created_at: doc.created_at,
- updated_at: doc.updated_at,
- content: doc.content,
- document_type: doc.document_type || 'document'
- }));
-
- setDocuments(projectDocuments);
-
- // Auto-select first document if available and no document is currently selected
- if (projectDocuments.length > 0 && !selectedDocument) {
- setSelectedDocument(projectDocuments[0]);
- }
-
- console.log(`Loaded ${projectDocuments.length} documents from project data`);
- } catch (error) {
- console.error('Failed to load documents:', error);
- showToast('Failed to load documents', 'error');
- } finally {
- setLoading(false);
- }
- };
-
- // 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
- return () => {
- console.log('🧹 DocsTab: Cleanup');
- // Polling cleanup happens automatically in hooks
- };
- }, [project?.id]);
-
- // Clear selected document when project changes
- useEffect(() => {
- setSelectedDocument(null);
- }, [project?.id]);
-
- // Handle crawl progress updates
- useEffect(() => {
- if (crawlProgress.data) {
- const status = crawlProgress.data.status;
- console.log('📊 Crawl progress update:', crawlProgress.data);
-
- if (status === 'completed') {
- showToast('Crawling completed successfully', 'success');
- loadKnowledgeItems(); // Reload knowledge items
- setActiveProgressId(null); // Clear active progress
- } else if (status === 'failed' || status === 'error') {
- const errorMsg = crawlProgress.data.error || 'Crawling failed';
- showToast(`Crawling failed: ${errorMsg}`, 'error');
- setActiveProgressId(null); // Clear active progress
- }
- }
- }, [crawlProgress.data, showToast]);
-
- // 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 handleStartCrawl = async (progressId: string, initialData: any) => {
- console.log(`🚀 Starting crawl tracking for: ${progressId}`);
- setActiveProgressId(progressId);
- showToast('Crawling started - tracking progress', 'success');
- };
-
- const openAddSourceModal = (type: 'technical' | 'business') => {
- setSourceType(type);
- setShowAddSourceModal(true);
- };
-
- return (
-
-
- {/* Document Header */}
-
-
- {/* Document Cards Container */}
-
-
- {documents.map(doc => (
- {
- try {
- // Call API to delete from database first
- await projectService.deleteDocument(project.id, docId);
-
- // Then remove from local state
- setDocuments(prev => prev.filter(d => d.id !== docId));
- if (selectedDocument?.id === docId) {
- setSelectedDocument(documents.find(d => d.id !== docId) || null);
- }
- showToast('Document deleted', 'success');
- } catch (error) {
- console.error('Failed to delete document:', error);
- showToast('Failed to delete document', 'error');
- }
- }}
- isDarkMode={isDarkMode}
- />
- ))}
-
- {/* Add New Document Card */}
- setShowTemplateModal(true)} />
-
-
-
- {/* Document Content */}
- {loading ? (
-
- ) : 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]) => (
-
onSelectTemplate(key)}
- disabled={isCreating}
- className="p-4 text-left border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed group"
- >
-
-
{template.icon}
-
-
{template.name}
-
- {getTemplateDescription(key, template)}
-
- {template.document_type === 'prp' && (
-
-
- PRP 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)]'
- },
- 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)]'
- },
- 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)]'
- },
- 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)]'
- }
- };
- return
-
-
-
- {title}
-
-
-
- Add Sources
-
-
-
-
- {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" />
-
-
-
- Add Source
-
-
- {/* 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 */}
-
-
- Cancel
-
-
- Save Selected ({selectedSources.length})
-
-
-
-
-
;
-};
-
-interface AddKnowledgeModalProps {
- sourceType: 'technical' | 'business';
- onClose: () => void;
- onSuccess: () => void;
- onStartCrawl: (progressId: string, initialData: any) => 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 progress tracking
- if ((result as any).progressId) {
- // Start progress tracking via polling
- 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 immediate 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 */}
-
- setMethod('url')} className={`flex-1 p-4 rounded-md border ${method === 'url' ? 'border-blue-500 text-blue-600 dark:text-blue-500 bg-blue-50 dark:bg-blue-500/5' : 'border-gray-200 dark:border-zinc-900 text-gray-500 dark:text-zinc-400 hover:border-blue-300 dark:hover:border-blue-500/30'} transition flex items-center justify-center gap-2`}>
-
- URL / Website
-
- setMethod('file')} className={`flex-1 p-4 rounded-md border ${method === 'file' ? 'border-pink-500 text-pink-600 dark:text-pink-500 bg-pink-50 dark:bg-pink-500/5' : 'border-gray-200 dark:border-zinc-900 text-gray-500 dark:text-zinc-400 hover:border-pink-300 dark:hover:border-pink-500/30'} transition flex items-center justify-center gap-2`}>
-
- Upload File
-
-
-
- {/* URL Input */}
- {method === 'url' &&
- setUrl(e.target.value)} placeholder="https://..." accentColor="blue" />
-
}
-
- {/* File Upload */}
- {method === 'file' &&
-
- Upload Document
-
-
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' &&
- setUpdateFrequency(e.target.value)} options={[{
- value: '1',
- label: 'Daily'
- }, {
- value: '7',
- label: 'Weekly'
- }, {
- value: '30',
- label: 'Monthly'
- }, {
- value: '0',
- label: 'Never'
- }]} accentColor="blue" />
-
}
-
- {/* Tags */}
-
-
- Tags
-
-
- {tags.map(tag =>
- {tag}
- )}
-
-
setNewTag(e.target.value)} onKeyDown={e => {
- if (e.key === 'Enter' && newTag.trim()) {
- setTags([...tags, newTag.trim()]);
- setNewTag('');
- }
- }} placeholder="Add tags..." accentColor="purple" />
-
-
- {/* Action Buttons */}
-
-
- Cancel
-
-
- {loading ? 'Adding...' : 'Add Source'}
-
-
-
-
;
-};
\ 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
deleted file mode 100644
index e6ec5b9b70..0000000000
--- a/archon-ui-main/src/components/project-tasks/DocumentCard.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import React, { useState } from 'react';
-import { Rocket, Code, Briefcase, Users, FileText, X, Plus, Clipboard } from 'lucide-react';
-import { useToast } from '../../contexts/ToastContext';
-
-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;
-}
-
-export const DocumentCard: React.FC = ({
- document,
- isActive,
- onSelect,
- onDelete,
- isDarkMode
-}) => {
- 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 = (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);
- };
-
- 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 */}
-
-
- {document.id.slice(0, 8)}...
-
-
-
-
-
-
- {/* Delete Button */}
- {showDelete && !isActive && (
-
{
- e.stopPropagation();
- if (confirm(`Delete "${document.title}"?`)) {
- onDelete(document.id);
- }
- }}
- className="absolute top-2 right-2 p-1 rounded-md bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-400 transition-colors"
- aria-label={`Delete ${document.title}`}
- title="Delete document"
- >
-
-
- )}
-
- );
-};
-
-// New Document Card Component
-interface NewDocumentCardProps {
- onClick: () => void;
-}
-
-export const NewDocumentCard: React.FC = ({ onClick }) => {
- return (
-
- );
-};
\ 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
deleted file mode 100644
index 62435e3bec..0000000000
--- a/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-import React, { useRef, useState } from 'react';
-import { useDrag, useDrop } from 'react-dnd';
-import { Edit, Trash2, RefreshCw, Tag, Clipboard } from 'lucide-react';
-import { Task } from './TaskTableView';
-import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
-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;
- hoveredTaskId?: string | null;
- onTaskHover?: (taskId: string | null) => void;
- selectedTasks?: Set;
- onTaskSelect?: (taskId: string) => void;
-}
-
-export const DraggableTaskCard = ({
- task,
- index,
- onView,
- onComplete,
- onDelete,
- onTaskReorder,
- hoveredTaskId,
- onTaskHover,
- selectedTasks,
- onTaskSelect,
-}: 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;
-
-
- // 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);
- };
-
- const isHighlighted = hoveredTaskId === task.id;
- const isSelected = selectedTasks?.has(task.id) || false;
-
- const handleMouseEnter = () => {
- onTaskHover?.(task.id);
- };
-
- const handleMouseLeave = () => {
- onTaskHover?.(null);
- };
-
- const handleTaskClick = (e: React.MouseEvent) => {
- if (e.ctrlKey || e.metaKey) {
- e.stopPropagation();
- onTaskSelect?.(task.id);
- }
- };
-
-
- // Card styling - using CSS-based height animation for better scrolling
-
- // Card styling constants
- const cardScale = 'scale-100';
- const cardOpacity = 'opacity-100';
-
- // 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)]'
- : '';
-
- // Selection styling
- const selectionGlow = isSelected
- ? 'border-blue-500 shadow-[0_0_12px_rgba(59,130,246,0.4)] bg-blue-50/30 dark:bg-blue-900/20'
- : '';
-
- // 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))}
- 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}
- onClick={handleTaskClick}
- >
-
- {/* 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 */}
-
- {
- e.stopPropagation();
- onDelete(task);
- }}
- className="w-5 h-5 rounded-full flex items-center justify-center bg-red-100/80 dark:bg-red-500/20 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-500/30 hover:shadow-[0_0_10px_rgba(239,68,68,0.3)] transition-all duration-300"
- title="Delete task"
- aria-label="Delete task"
- >
-
-
- {
- e.stopPropagation();
- onView();
- }}
- className="w-5 h-5 rounded-full flex items-center justify-center bg-cyan-100/80 dark:bg-cyan-500/20 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-200 dark:hover:bg-cyan-500/30 hover:shadow-[0_0_10px_rgba(34,211,238,0.3)] transition-all duration-300"
- title="Edit task"
- aria-label="Edit task"
- >
-
-
-
-
-
-
-
-
-
- {task.title}
-
-
- {/* Spacer to push assignee section to bottom */}
-
-
-
-
-
- {getAssigneeIcon(task.assignee?.name || 'User')}
-
-
{task.assignee?.name || 'User'}
-
-
{
- e.stopPropagation();
- try {
- if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(task.id);
- } else {
- const ta = document.createElement('textarea');
- ta.value = task.id;
- ta.style.position = 'fixed';
- ta.style.opacity = '0';
- document.body.appendChild(ta);
- ta.select();
- document.execCommand('copy');
- document.body.removeChild(ta);
- }
- showToast('Task ID copied to clipboard', 'success');
- } catch (error) {
- showToast('Failed to copy Task ID', 'error');
- }
- }}
- className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
- title="Copy Task ID to clipboard"
- aria-label="Copy Task ID to clipboard"
- >
-
- Task ID
-
-
-
-
-
- {/* 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 */}
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx b/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx
deleted file mode 100644
index fc00312c37..0000000000
--- a/archon-ui-main/src/components/project-tasks/EditTaskModal.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from 'react';
-import { X } from 'lucide-react';
-import { Button } from '../ui/Button';
-import { ArchonLoadingSpinner } from '../animations/Animations';
-import { DebouncedInput, FeatureInput } from './TaskInputComponents';
-import type { Task } from './TaskTableView';
-
-interface EditTaskModalProps {
- isModalOpen: boolean;
- editingTask: Task | null;
- projectFeatures: any[];
- isLoadingFeatures: boolean;
- isSavingTask: boolean;
- onClose: () => void;
- onSave: (task: Task) => Promise;
- getTasksForPrioritySelection: (status: Task['status']) => Array<{value: number, label: string}>;
-}
-
-const ASSIGNEE_OPTIONS = ['User', 'Archon', 'AI IDE Agent'] as const;
-
-// Removed debounce utility - now using DebouncedInput component
-
-export const EditTaskModal = memo(({
- isModalOpen,
- editingTask,
- projectFeatures,
- isLoadingFeatures,
- isSavingTask,
- onClose,
- onSave,
- getTasksForPrioritySelection
-}: EditTaskModalProps) => {
- const [localTask, setLocalTask] = useState(null);
-
- // Diagnostic: Track render count
- const renderCount = useRef(0);
-
- useEffect(() => {
- renderCount.current++;
- console.log(`[EditTaskModal] Render #${renderCount.current}`, {
- localTask: localTask?.title,
- isModalOpen,
- timestamp: Date.now()
- });
- });
-
- // Sync local state with editingTask when it changes
- useEffect(() => {
- if (editingTask) {
- setLocalTask(editingTask);
- }
- }, [editingTask]);
-
- const priorityOptions = useMemo(() => {
- console.log(`[EditTaskModal] Recalculating priorityOptions for status: ${localTask?.status || 'todo'}`);
- return getTasksForPrioritySelection(localTask?.status || 'todo');
- }, [localTask?.status, getTasksForPrioritySelection]);
-
- // Memoized handlers for input changes
- const handleTitleChange = useCallback((value: string) => {
- console.log('[EditTaskModal] Title changed via DebouncedInput:', value);
- setLocalTask(prev => prev ? { ...prev, title: value } : null);
- }, []);
-
- const handleDescriptionChange = useCallback((value: string) => {
- console.log('[EditTaskModal] Description changed via DebouncedInput:', value);
- setLocalTask(prev => prev ? { ...prev, description: value } : null);
- }, []);
-
- const handleFeatureChange = useCallback((value: string) => {
- console.log('[EditTaskModal] Feature changed via FeatureInput:', value);
- setLocalTask(prev => prev ? { ...prev, feature: value } : null);
- }, []);
-
- const handleStatusChange = useCallback((e: React.ChangeEvent) => {
- const newStatus = e.target.value as Task['status'];
- const newOrder = getTasksForPrioritySelection(newStatus)[0]?.value || 1;
- setLocalTask(prev => prev ? { ...prev, status: newStatus, task_order: newOrder } : null);
- }, [getTasksForPrioritySelection]);
-
- const handlePriorityChange = useCallback((e: React.ChangeEvent) => {
- setLocalTask(prev => prev ? { ...prev, task_order: parseInt(e.target.value) } : null);
- }, []);
-
- const handleAssigneeChange = useCallback((e: React.ChangeEvent) => {
- setLocalTask(prev => prev ? {
- ...prev,
- assignee: { name: e.target.value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' }
- } : null);
- }, []);
-
- const handleSave = useCallback(() => {
- if (localTask) {
- onSave(localTask);
- }
- }, [localTask, onSave]);
-
- const handleClose = useCallback(() => {
- onClose();
- }, [onClose]);
-
- if (!isModalOpen) return null;
-
- return (
-
-
-
-
-
- {editingTask?.id ? 'Edit Task' : 'New Task'}
-
-
-
-
-
-
-
-
- Title
-
-
-
-
- Description
-
-
-
-
-
- Status
-
- Todo
- Doing
- Review
- Done
-
-
-
-
- Priority
-
- {priorityOptions.map((option) => (
- {option.label}
- ))}
-
-
-
-
-
-
- Assignee
-
- {ASSIGNEE_OPTIONS.map(option => (
- {option}
- ))}
-
-
-
-
- Feature
-
-
-
-
-
-
-
-
Cancel
-
- {isSavingTask ? (
-
-
- {localTask?.id ? 'Saving...' : 'Creating...'}
-
- ) : (
- localTask?.id ? 'Save Changes' : 'Create Task'
- )}
-
-
-
-
-
- );
-}, (prevProps, nextProps) => {
- // Custom comparison function to prevent unnecessary re-renders
- // Only re-render if these specific props change
- const isEqual = (
- prevProps.isModalOpen === nextProps.isModalOpen &&
- prevProps.editingTask?.id === nextProps.editingTask?.id &&
- prevProps.editingTask?.title === nextProps.editingTask?.title &&
- prevProps.editingTask?.description === nextProps.editingTask?.description &&
- prevProps.editingTask?.status === nextProps.editingTask?.status &&
- prevProps.editingTask?.assignee?.name === nextProps.editingTask?.assignee?.name &&
- prevProps.editingTask?.feature === nextProps.editingTask?.feature &&
- prevProps.editingTask?.task_order === nextProps.editingTask?.task_order &&
- prevProps.isSavingTask === nextProps.isSavingTask &&
- prevProps.isLoadingFeatures === nextProps.isLoadingFeatures &&
- prevProps.projectFeatures === nextProps.projectFeatures // Reference equality check
- );
-
- if (!isEqual) {
- console.log('[EditTaskModal] Props changed, re-rendering');
- }
-
- return isEqual;
-});
-
-EditTaskModal.displayName = 'EditTaskModal';
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/FeaturesTab.tsx b/archon-ui-main/src/components/project-tasks/FeaturesTab.tsx
deleted file mode 100644
index 10add1b4dc..0000000000
--- a/archon-ui-main/src/components/project-tasks/FeaturesTab.tsx
+++ /dev/null
@@ -1,814 +0,0 @@
-import { useCallback, useState, useEffect, useMemo } from 'react'
-import '@xyflow/react/dist/style.css'
-import {
- ReactFlow,
- Node,
- Edge,
- Controls,
- MarkerType,
- NodeProps,
- Handle,
- Position,
- NodeChange,
- applyNodeChanges,
- EdgeChange,
- applyEdgeChanges,
- Connection,
- addEdge,
-} from '@xyflow/react'
-import { Layout, Component as ComponentIcon, X, Trash2, Edit, Save } from 'lucide-react'
-import { projectService } from '../../services/projectService'
-import { useToast } from '../../contexts/ToastContext'
-
-// Define custom node types following React Flow v12 pattern
-type PageNodeData = {
- label: string;
- type: string;
- route: string;
- components: number;
-};
-
-type ServiceNodeData = {
- label: string;
- type: string;
-};
-
-// Define union type for all custom nodes
-type CustomNodeTypes = Node | Node;
-
-// Custom node components
-const PageNode = ({ data }: NodeProps) => {
- const pageData = data as PageNodeData;
- return (
-
-
-
-
-
{pageData.type}
-
-
Route: {pageData.route}
-
Components: {pageData.components}
-
-
-
-
- );
-};
-
-const ServiceNode = ({ data }: NodeProps) => {
- const serviceData = data as ServiceNodeData;
- return (
-
-
-
-
-
-
{serviceData.label}
-
-
{serviceData.type}
-
-
-
- );
-};
-
-const nodeTypes = {
- page: PageNode,
- service: ServiceNode,
-}
-
-// Default/fallback nodes for when project has no features data
-const defaultNodes: Node[] = [
- {
- id: 'start',
- type: 'page',
- data: {
- label: 'Start App',
- type: 'Entry Point',
- route: '/',
- components: 3,
- },
- position: {
- x: 400,
- y: 0,
- },
- },
- {
- id: 'home',
- type: 'page',
- data: {
- label: 'Homepage',
- type: 'Main View',
- route: '/home',
- components: 6,
- },
- position: {
- x: 400,
- y: 150,
- },
- },
-];
-
-// Default/fallback edges
-const defaultEdges: Edge[] = [
- {
- id: 'start-home',
- source: 'start',
- target: 'home',
- animated: true,
- style: {
- stroke: '#22d3ee',
- },
- markerEnd: {
- type: MarkerType.ArrowClosed,
- color: '#22d3ee',
- },
- },
-];
-
-interface FeaturesTabProps {
- project?: {
- id: string;
- title: string;
- features?: any[];
- } | null;
-}
-
-export const FeaturesTab = ({ project }: FeaturesTabProps) => {
- const [nodes, setNodes] = useState([])
- const [edges, setEdges] = useState([])
- const [loading, setLoading] = useState(true)
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
- const [nodeToDelete, setNodeToDelete] = useState(null)
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
- const [editingNode, setEditingNode] = useState(null)
- const [showEditModal, setShowEditModal] = useState(false)
- const [isSaving, setIsSaving] = useState(false)
-
- const { showToast } = useToast()
-
- // Load features from project or show empty state
- useEffect(() => {
- if (project?.features && Array.isArray(project.features) && project.features.length > 0) {
- // Ensure all nodes have required properties with defaults
- const normalizedNodes = project.features.map((node: any, index: number) => ({
- ...node,
- // Ensure position exists with sensible defaults
- position: node.position || {
- x: 250 + (index % 3) * 250, // Spread horizontally
- y: 200 + Math.floor(index / 3) * 150 // Stack vertically
- },
- // Ensure type exists (fallback based on data structure)
- type: node.type || (node.data?.route ? 'page' : 'service'),
- // Ensure data exists
- data: node.data || { label: 'Unknown', type: 'Unknown Component' }
- }));
-
- setNodes(normalizedNodes)
- // Generate edges based on the flow (simplified logic)
- const generatedEdges = generateEdgesFromNodes(normalizedNodes)
- setEdges(generatedEdges)
- } else {
- // Show empty state - no nodes or edges
- setNodes([])
- setEdges([])
- }
- setLoading(false)
- }, [project])
-
- // Helper function to generate edges based on node positioning and types
- const generateEdgesFromNodes = (nodes: Node[]): Edge[] => {
- const edges: Edge[] = []
-
- // Sort nodes by y position to create a logical flow (with safety check for position)
- const sortedNodes = [...nodes].sort((a, b) => {
- const aY = a.position?.y || 0;
- const bY = b.position?.y || 0;
- return aY - bY;
- })
-
- for (let i = 0; i < sortedNodes.length - 1; i++) {
- const currentNode = sortedNodes[i]
- const nextNode = sortedNodes[i + 1]
-
- // Connect sequential nodes with appropriate styling
- const edgeStyle = currentNode.type === 'service' ? '#d946ef' : '#22d3ee'
-
- edges.push({
- id: `${currentNode.id}-${nextNode.id}`,
- source: currentNode.id,
- target: nextNode.id,
- animated: true,
- style: {
- stroke: edgeStyle,
- },
- markerEnd: {
- type: MarkerType.ArrowClosed,
- color: edgeStyle,
- },
- })
- }
-
- return edges
- }
-
- const onNodesChange = useCallback(
- (changes: NodeChange[]) => {
- setNodes((nds) => applyNodeChanges(changes, nds))
- setHasUnsavedChanges(true)
- },
- [],
- )
-
- const onEdgesChange = useCallback(
- (changes: EdgeChange[]) => {
- setEdges((eds) => applyEdgeChanges(changes, eds))
- setHasUnsavedChanges(true)
- },
- [],
- )
-
- const onConnect = useCallback(
- (connection: Connection) => {
- const sourceNode = nodes.find((node) => node.id === connection.source)
- // Set edge color based on source node type
- const edgeStyle =
- sourceNode?.type === 'service'
- ? {
- stroke: '#d946ef',
- }
- : // Fuchsia for service nodes
- {
- stroke: '#22d3ee',
- } // Cyan for page nodes
- setEdges((eds) =>
- addEdge(
- {
- ...connection,
- animated: true,
- style: edgeStyle,
- markerEnd: {
- type: MarkerType.ArrowClosed,
- color: edgeStyle.stroke,
- },
- },
- eds,
- ),
- )
- setHasUnsavedChanges(true)
- },
- [nodes],
- )
-
- const saveToDatabase = async (nodesToSave = nodes, edgesToSave = edges) => {
- if (!project?.id) {
- console.error('❌ No project ID available for saving features');
- return;
- }
-
- setIsSaving(true);
- try {
- console.log('💾 Saving features to database...');
- await projectService.updateProject(project.id, {
- features: nodesToSave
- });
- console.log('✅ Features saved successfully');
- setHasUnsavedChanges(false);
- } catch (error) {
- console.error('❌ Failed to save features:', error);
- throw error;
- } finally {
- setIsSaving(false);
- }
- };
-
- const handleManualSave = async () => {
- await saveToDatabase();
- };
-
- const addPageNode = async () => {
- const newNode: Node = {
- id: `page-${Date.now()}`,
- type: 'page',
- data: {
- label: `New Page`,
- type: 'Page Component',
- route: '/new-page',
- components: 0,
- },
- position: {
- x: 250,
- y: 200,
- },
- }
- const newNodes = [...nodes, newNode];
- setNodes(newNodes);
- setHasUnsavedChanges(true);
-
- // Auto-save when adding
- try {
- await saveToDatabase(newNodes, edges);
- } catch (error) {
- // Revert on error
- setNodes(nodes);
- }
- }
-
- const addServiceNode = async () => {
- const newNode: Node = {
- id: `service-${Date.now()}`,
- type: 'service',
- data: {
- label: 'New Service',
- type: 'Service Component',
- },
- position: {
- x: 250,
- y: 200,
- },
- }
- const newNodes = [...nodes, newNode];
- setNodes(newNodes);
- setHasUnsavedChanges(true);
-
- // Auto-save when adding
- try {
- await saveToDatabase(newNodes, edges);
- } catch (error) {
- // Revert on error
- setNodes(nodes);
- }
- }
-
- const handleDeleteNode = useCallback(async (event: React.MouseEvent, nodeId: string) => {
- event.stopPropagation();
-
- if (!project?.id) {
- console.error('❌ No project ID available for deleting node');
- return;
- }
-
- // Show custom confirmation dialog
- setNodeToDelete(nodeId);
- setShowDeleteConfirm(true);
- }, [project?.id]);
-
- const confirmDelete = useCallback(async () => {
- if (!nodeToDelete) return;
-
- console.log('🗑️ Deleting node:', nodeToDelete);
-
- try {
- // Remove node from UI
- const newNodes = nodes.filter(node => node.id !== nodeToDelete);
-
- // Remove any edges connected to this node
- const newEdges = edges.filter(edge =>
- edge.source !== nodeToDelete && edge.target !== nodeToDelete
- );
-
- setNodes(newNodes);
- setEdges(newEdges);
-
- // Save to database
- await saveToDatabase(newNodes, newEdges);
- showToast('Node deleted successfully', 'success');
-
- // Close confirmation dialog
- setShowDeleteConfirm(false);
- setNodeToDelete(null);
- } catch (error) {
- console.error('❌ Failed to delete node:', error);
- // Revert UI changes on error
- setNodes(nodes);
- setEdges(edges);
- showToast('Failed to delete node', 'error');
- }
- }, [nodeToDelete, nodes, edges]);
-
- const cancelDelete = useCallback(() => {
- setShowDeleteConfirm(false);
- setNodeToDelete(null);
- }, []);
-
- const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
- setEditingNode(node);
- setShowEditModal(true);
- }, []);
-
- const saveNodeChanges = async (updatedNode: Node) => {
- // Update local state first
- const newNodes = nodes.map(node =>
- node.id === updatedNode.id ? updatedNode : node
- );
- setNodes(newNodes);
-
- // Save to database
- await saveToDatabase(newNodes, edges);
-
- setShowEditModal(false);
- setEditingNode(null);
- };
-
- // Memoize node types with delete and edit functionality
- const nodeTypes = useMemo(() => ({
- page: ({ data, id }: NodeProps) => {
- const pageData = data as any;
- return (
-
-
-
{
- const actualNode = nodes.find(node => node.id === id);
- if (actualNode) {
- handleNodeClick(e, actualNode);
- }
- }}
- >
-
-
-
- {
- e.stopPropagation();
- const actualNode = nodes.find(node => node.id === id);
- if (actualNode) {
- handleNodeClick(e, actualNode);
- }
- }}
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-cyan-600/20 rounded"
- title="Edit node"
- >
-
-
- handleDeleteNode(e, id)}
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-600/20 rounded"
- title="Delete node"
- >
-
-
-
-
-
{pageData.type}
-
-
Route: {pageData.route}
-
Components: {pageData.components}
-
-
-
-
- );
- },
- service: ({ data, id }: NodeProps) => {
- const serviceData = data as any;
- return (
-
-
-
{
- const actualNode = nodes.find(node => node.id === id);
- if (actualNode) {
- handleNodeClick(e, actualNode);
- }
- }}
- >
-
-
-
-
{serviceData.label}
-
-
- {
- e.stopPropagation();
- const actualNode = nodes.find(node => node.id === id);
- if (actualNode) {
- handleNodeClick(e, actualNode);
- }
- }}
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-fuchsia-600/20 rounded"
- title="Edit node"
- >
-
-
- handleDeleteNode(e, id)}
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-600/20 rounded"
- title="Delete node"
- >
-
-
-
-
-
{serviceData.type}
-
-
-
- );
- }
- }), [handleNodeClick, handleDeleteNode, nodes]);
-
- if (loading) {
- return (
-
- )
- }
-
- return (
-
-
-
-
-
-
- Feature Planner {project?.features ? `(${project.features.length} features)` : '(Default)'}
-
-
- {hasUnsavedChanges && (
-
-
- {isSaving ? 'Saving...' : 'Save Layout'}
-
- )}
-
-
-
- Add Page
-
-
-
-
- Add Service
-
-
-
-
- {/* Subtle neon glow at the top */}
-
- {nodes.length === 0 ? (
-
-
-
No features defined
-
Add pages and services to get started
-
- ) : (
-
-
-
- )}
-
-
- {/* Delete Confirmation Modal */}
- {showDeleteConfirm && (
-
n.id === nodeToDelete)?.data.label as string || 'node'}
- />
- )}
-
- {/* Edit Modal */}
- {showEditModal && editingNode && (
- {
- setShowEditModal(false);
- setEditingNode(null);
- }}
- />
- )}
-
-
- )
-}
-
-// Delete Confirmation Modal Component
-const DeleteConfirmModal = ({
- onConfirm,
- onCancel,
- nodeName
-}: {
- onConfirm: () => void;
- onCancel: () => void;
- nodeName: string;
-}) => {
- return (
-
-
-
-
-
-
-
-
-
-
- Delete Node
-
-
- This action cannot be undone
-
-
-
-
-
- Are you sure you want to delete "{nodeName}" ?
- This will also remove all related connections.
-
-
-
-
- Cancel
-
-
- Delete Node
-
-
-
-
-
- );
-};
-
-// Edit Feature Modal Component
-const EditFeatureModal = ({
- node,
- onSave,
- onClose
-}: {
- node: Node;
- onSave: (node: Node) => void;
- onClose: () => void;
-}) => {
- const [name, setName] = useState(node.data.label as string);
- const [route, setRoute] = useState((node.data as any).route || '');
- const [components, setComponents] = useState((node.data as any).components || 0);
-
- const isPageNode = node.type === 'page';
-
- const handleSave = () => {
- const updatedNode = {
- ...node,
- data: {
- ...node.data,
- label: name,
- ...(isPageNode && { route, components })
- }
- };
- onSave(updatedNode);
- };
-
- return (
-
-
-
-
- {isPageNode ? : }
- Edit {isPageNode ? 'Page' : 'Service'}
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
- Save Changes
-
-
-
-
- );
-};
diff --git a/archon-ui-main/src/components/project-tasks/MilkdownEditor.css b/archon-ui-main/src/components/project-tasks/MilkdownEditor.css
deleted file mode 100644
index c9c272d7af..0000000000
--- a/archon-ui-main/src/components/project-tasks/MilkdownEditor.css
+++ /dev/null
@@ -1,277 +0,0 @@
-/* Milkdown Editor Custom Styles - Archon Theme */
-
-/* Main editor container */
-.milkdown-crepe-editor {
- background: rgba(255, 255, 255, 0.5);
- backdrop-filter: blur(10px);
- border: 1px solid rgba(59, 130, 246, 0.3);
- border-radius: 12px;
- padding: 1.5rem;
- position: relative;
- overflow: hidden;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
-}
-
-/* Gradient border effect */
-.milkdown-crepe-editor::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: linear-gradient(90deg, #3b82f6, #a855f7);
- opacity: 0.8;
-}
-
-/* Dark mode container */
-.dark .milkdown-crepe-editor {
- background: rgba(0, 0, 0, 0.3);
- backdrop-filter: blur(20px);
- border-color: rgba(59, 130, 246, 0.5);
- box-shadow: 0 0 20px rgba(59, 130, 246, 0.1);
-}
-
-/* Remove default Crepe theme styling */
-.milkdown-crepe-editor .milkdown {
- background: transparent !important;
- border: none !important;
- box-shadow: none !important;
-}
-
-/* Editor content area */
-.milkdown-crepe-editor .ProseMirror {
- font-family: Inter, system-ui, -apple-system, sans-serif;
- min-height: 400px;
- max-width: 100%;
- padding: 1rem;
- background: transparent;
- color: #1f2937;
- line-height: 1.6;
-}
-
-.dark .milkdown-crepe-editor .ProseMirror {
- color: #f9fafb;
-}
-
-/* Remove dark mode filter - use proper theming instead */
-.milkdown-theme-dark {
- filter: none;
-}
-
-/* Typography */
-.milkdown-crepe-editor h1 {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 1rem;
- color: #111827;
-}
-
-.dark .milkdown-crepe-editor h1 {
- color: #f9fafb;
-}
-
-.milkdown-crepe-editor h2 {
- font-size: 1.5rem;
- font-weight: 600;
- margin-top: 1.5rem;
- margin-bottom: 0.75rem;
- color: #374151;
-}
-
-.dark .milkdown-crepe-editor h2 {
- color: #e5e7eb;
-}
-
-.milkdown-crepe-editor h3 {
- font-size: 1.25rem;
- font-weight: 600;
- margin-top: 1rem;
- margin-bottom: 0.5rem;
- color: #4b5563;
-}
-
-.dark .milkdown-crepe-editor h3 {
- color: #d1d5db;
-}
-
-/* Links */
-.milkdown-crepe-editor a {
- color: #3b82f6;
- text-decoration: none;
- transition: color 0.2s;
-}
-
-.milkdown-crepe-editor a:hover {
- color: #2563eb;
- text-decoration: underline;
-}
-
-.dark .milkdown-crepe-editor a {
- color: #60a5fa;
-}
-
-.dark .milkdown-crepe-editor a:hover {
- color: #93bbfc;
-}
-
-/* Code blocks */
-.milkdown-crepe-editor pre {
- background: rgba(0, 0, 0, 0.05);
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 8px;
- padding: 1rem;
- overflow-x: auto;
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
-}
-
-.dark .milkdown-crepe-editor pre {
- background: rgba(255, 255, 255, 0.05);
- border-color: rgba(255, 255, 255, 0.1);
-}
-
-.milkdown-crepe-editor code {
- background: rgba(59, 130, 246, 0.1);
- padding: 0.125rem 0.375rem;
- border-radius: 4px;
- font-size: 0.875rem;
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
-}
-
-.dark .milkdown-crepe-editor code {
- background: rgba(59, 130, 246, 0.2);
-}
-
-/* Lists */
-.milkdown-crepe-editor ul,
-.milkdown-crepe-editor ol {
- padding-left: 1.5rem;
- margin: 0.5rem 0;
-}
-
-.milkdown-crepe-editor li {
- margin: 0.25rem 0;
-}
-
-/* Blockquotes */
-.milkdown-crepe-editor blockquote {
- border-left: 4px solid #3b82f6;
- padding-left: 1rem;
- margin: 1rem 0;
- color: #6b7280;
- font-style: italic;
-}
-
-.dark .milkdown-crepe-editor blockquote {
- color: #9ca3af;
- border-left-color: #60a5fa;
-}
-
-/* Tables */
-.milkdown-crepe-editor table {
- border-collapse: collapse;
- width: 100%;
- margin: 1rem 0;
-}
-
-.milkdown-crepe-editor th,
-.milkdown-crepe-editor td {
- border: 1px solid rgba(0, 0, 0, 0.1);
- padding: 0.5rem;
- text-align: left;
-}
-
-.dark .milkdown-crepe-editor th,
-.dark .milkdown-crepe-editor td {
- border-color: rgba(255, 255, 255, 0.1);
-}
-
-.milkdown-crepe-editor th {
- background: rgba(59, 130, 246, 0.05);
- font-weight: 600;
-}
-
-.dark .milkdown-crepe-editor th {
- background: rgba(59, 130, 246, 0.1);
-}
-
-/* Toolbar styling */
-.milkdown-crepe-editor .milkdown-toolbar {
- background: transparent;
- border: none;
- padding: 0;
- margin-bottom: 1rem;
-}
-
-/* Toolbar buttons */
-.milkdown-crepe-editor .toolbar-item {
- background: rgba(255, 255, 255, 0.8);
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 6px;
- padding: 0.375rem 0.75rem;
- margin: 0 0.25rem;
- cursor: pointer;
- transition: all 0.2s;
- color: #374151;
-}
-
-.dark .milkdown-crepe-editor .toolbar-item {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.2);
- color: #e5e7eb;
-}
-
-.milkdown-crepe-editor .toolbar-item:hover {
- background: #3b82f6;
- border-color: #3b82f6;
- color: white;
- transform: translateY(-1px);
- box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
-}
-
-/* Selection */
-.milkdown-crepe-editor .ProseMirror ::selection {
- background: rgba(59, 130, 246, 0.3);
-}
-
-.dark .milkdown-crepe-editor .ProseMirror ::selection {
- background: rgba(96, 165, 250, 0.3);
-}
-
-/* Focus state */
-.milkdown-crepe-editor .ProseMirror:focus {
- outline: none;
-}
-
-/* Placeholder */
-.milkdown-crepe-editor .ProseMirror.is-empty::before {
- content: 'Start writing...';
- color: #9ca3af;
- position: absolute;
- pointer-events: none;
-}
-
-/* Horizontal rule */
-.milkdown-crepe-editor hr {
- border: none;
- height: 1px;
- background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.5), transparent);
- margin: 2rem 0;
-}
-
-/* Save button animation */
-@keyframes pulse-glow {
- 0% {
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
- }
- 70% {
- box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
- }
-}
-
-.save-button-pulse {
- animation: pulse-glow 2s infinite;
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/MilkdownEditor.tsx b/archon-ui-main/src/components/project-tasks/MilkdownEditor.tsx
deleted file mode 100644
index 6f945ccb54..0000000000
--- a/archon-ui-main/src/components/project-tasks/MilkdownEditor.tsx
+++ /dev/null
@@ -1,555 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react';
-import { Crepe, CrepeFeature } from '@milkdown/crepe';
-import '@milkdown/crepe/theme/common/style.css';
-import '@milkdown/crepe/theme/frame.css';
-import '@milkdown/crepe/theme/frame-dark.css';
-import './MilkdownEditor.css';
-import { Save, Undo } from 'lucide-react';
-
-interface MilkdownEditorProps {
- document: {
- id: string;
- title: string;
- content?: any;
- created_at: string;
- updated_at: string;
- };
- onSave: (document: any) => void;
- className?: string;
- isDarkMode?: boolean;
-}
-
-export const MilkdownEditor: React.FC = ({
- document: doc,
- onSave,
- className = '',
- isDarkMode = false,
-}) => {
- const editorRef = useRef(null);
- const crepeRef = useRef(null);
- const [isLoading, setIsLoading] = useState(false);
- const [hasChanges, setHasChanges] = useState(false);
- const [isReverted, setIsReverted] = useState(false);
- const [originalContent, setOriginalContent] = useState('');
- const [currentContent, setCurrentContent] = useState('');
-
- // Convert document content to markdown string
- const getMarkdownContent = () => {
- if (typeof doc.content === 'string') {
- return doc.content;
- }
-
- if (doc.content && typeof doc.content === 'object') {
- // If content has a markdown field, use it
- if (doc.content.markdown) {
- return doc.content.markdown;
- }
-
- // Check if this is a PRP document
- if (doc.content.document_type === 'prp' || doc.document_type === 'prp') {
- return convertPRPToMarkdown(doc.content);
- }
-
- // Otherwise, convert the content object to a readable markdown format
- let markdown = `# ${doc.title}\n\n`;
-
- Object.entries(doc.content).forEach(([key, value]) => {
- const sectionTitle = key.replace(/_/g, ' ').charAt(0).toUpperCase() + key.replace(/_/g, ' ').slice(1);
- markdown += `## ${sectionTitle}\n\n`;
-
- if (Array.isArray(value)) {
- value.forEach(item => {
- markdown += `- ${item}\n`;
- });
- markdown += '\n';
- } else if (typeof value === 'object' && value !== null) {
- if (value.description) {
- markdown += `${value.description}\n\n`;
- } else {
- Object.entries(value).forEach(([subKey, subValue]) => {
- markdown += `**${subKey}:** ${subValue}\n\n`;
- });
- }
- } else {
- markdown += `${value}\n\n`;
- }
- });
-
- return markdown;
- }
-
- return `# ${doc.title}\n\nStart writing...`;
- };
-
- // Helper function to format values for markdown
- // Enhanced formatValue to handle complex nested structures
- const formatValue = (value: any, indent = '', depth = 0): string => {
- if (value === null || value === undefined) {
- return '';
- }
-
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
- return String(value);
- }
-
- if (Array.isArray(value)) {
- if (value.length === 0) return '';
-
- // Check if it's a simple array (strings/numbers)
- const isSimple = value.every(item =>
- typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
- );
-
- if (isSimple) {
- return value.map(item => `${indent}- ${item}`).join('\n') + '\n';
- }
-
- // Complex array with objects
- return value.map((item, index) => {
- if (typeof item === 'object' && item !== null) {
- const itemLines = formatValue(item, indent + ' ', depth + 1).split('\n');
- const firstLine = itemLines[0];
- const restLines = itemLines.slice(1).join('\n');
-
- if (itemLines.length === 1 || (itemLines.length === 2 && !itemLines[1])) {
- // Single line item
- return `${indent}- ${firstLine}`;
- } else {
- // Multi-line item
- return `${indent}-\n${indent} ${firstLine}${restLines ? '\n' + restLines : ''}`;
- }
- }
- return `${indent}- ${formatValue(item, indent + ' ', depth + 1)}`;
- }).join('\n') + '\n';
- }
-
- if (typeof value === 'object' && value !== null) {
- const entries = Object.entries(value);
- if (entries.length === 0) return '';
-
- // Check if it's a simple object (all values are primitives)
- const isSimple = entries.every(([_, val]) =>
- typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean'
- );
-
- if (isSimple && entries.length <= 3 && depth > 0) {
- // Inline simple objects
- const pairs = entries.map(([k, v]) => `${formatKey(k)}: ${v}`);
- return pairs.join(', ');
- }
-
- let result = '';
- entries.forEach(([key, val], index) => {
- const formattedKey = formatKey(key);
-
- if (val === null || val === undefined) {
- return; // Skip null/undefined
- }
-
- if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
- result += `${indent}**${formattedKey}:** ${val}\n`;
- } else if (Array.isArray(val)) {
- result += `${indent}**${formattedKey}:**\n${formatValue(val, indent, depth + 1)}`;
- } else if (typeof val === 'object') {
- // Use appropriate heading level based on depth
- const headingLevel = Math.min(depth + 3, 6);
- const heading = '#'.repeat(headingLevel);
- result += `${indent}${heading} ${formattedKey}\n\n${formatValue(val, indent, depth + 1)}`;
- }
-
- // Add spacing between top-level sections
- if (depth === 0 && index < entries.length - 1) {
- result += '\n';
- }
- });
-
- return result;
- }
-
- return String(value);
- };
-
- // Helper to format keys nicely
- const formatKey = (key: string): string => {
- return key
- .replace(/_/g, ' ')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .split(' ')
- .filter(word => word.length > 0)
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
- .join(' ');
- };
-
- // Convert PRP document structure to readable markdown - fully dynamic
- const convertPRPToMarkdown = (content: any): string => {
- // Handle raw string content
- if (typeof content === 'string') {
- return content;
- }
-
- // Handle null/undefined
- if (!content || typeof content !== 'object') {
- return `# ${doc.title}\n\nNo content available.`;
- }
-
- // Start with title
- let markdown = `# ${content.title || doc.title || 'Untitled Document'}\n\n`;
-
- // Group metadata fields
- const metadataFields = ['version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
- const metadata = metadataFields.filter(field => content[field]);
-
- if (metadata.length > 0) {
- markdown += `## Metadata\n\n`;
- metadata.forEach(field => {
- const value = content[field];
- const label = formatKey(field);
- markdown += `- **${label}:** ${value}\n`;
- });
- markdown += '\n';
- }
-
- // Process all other fields dynamically
- const skipFields = ['title', ...metadataFields, 'id', '_id', 'project_id'];
-
- // Sort fields by priority (known important fields first)
- const priorityFields = [
- 'goal', 'goals', 'objective', 'objectives',
- 'why', 'rationale', 'background',
- 'what', 'description', 'overview',
- 'context', 'background_context',
- 'user_personas', 'personas', 'users', 'stakeholders',
- 'user_flows', 'flows', 'journeys', 'workflows',
- 'requirements', 'functional_requirements', 'non_functional_requirements',
- 'success_metrics', 'metrics', 'kpis', 'success_criteria',
- 'timeline', 'roadmap', 'milestones', 'phases',
- 'implementation_plan', 'implementation_roadmap', 'plan',
- 'technical_requirements', 'technical_implementation', 'architecture',
- 'validation_gates', 'testing_strategy', 'quality_gates',
- 'risks', 'risk_assessment', 'mitigation_strategies'
- ];
-
- // Create ordered list of fields
- const orderedFields = [];
- const remainingFields = [];
-
- Object.keys(content).forEach(key => {
- if (skipFields.includes(key)) return;
-
- const lowerKey = key.toLowerCase();
- const priorityIndex = priorityFields.findIndex(pf =>
- lowerKey === pf || lowerKey.includes(pf) || pf.includes(lowerKey)
- );
-
- if (priorityIndex !== -1) {
- orderedFields.push({ key, priority: priorityIndex });
- } else {
- remainingFields.push(key);
- }
- });
-
- // Sort by priority
- orderedFields.sort((a, b) => a.priority - b.priority);
-
- // Process fields in order
- const allFields = [...orderedFields.map(f => f.key), ...remainingFields];
-
- allFields.forEach(key => {
- const value = content[key];
- if (value === null || value === undefined) return;
-
- const sectionTitle = formatKey(key);
- markdown += `## ${sectionTitle}\n\n`;
-
- // Handle different value types
- if (typeof value === 'string') {
- markdown += `${value}\n\n`;
- } else if (typeof value === 'number' || typeof value === 'boolean') {
- markdown += `${value}\n\n`;
- } else if (Array.isArray(value)) {
- markdown += formatValue(value) + '\n';
- } else if (typeof value === 'object') {
- markdown += formatValue(value) + '\n';
- }
- });
-
- return markdown.trim();
- };
-
- // Initialize editor
- useEffect(() => {
- if (!editorRef.current || crepeRef.current) return;
-
- const initialContent = getMarkdownContent();
- setOriginalContent(initialContent);
- setCurrentContent(initialContent);
-
- // Add theme class to root element
- if (isDarkMode) {
- editorRef.current.classList.add('milkdown-theme-dark');
- }
-
- const crepe = new Crepe({
- root: editorRef.current,
- defaultValue: initialContent,
- features: {
- [CrepeFeature.HeaderMeta]: true,
- [CrepeFeature.LinkTooltip]: true,
- [CrepeFeature.ImageBlock]: true,
- [CrepeFeature.BlockEdit]: true,
- [CrepeFeature.ListItem]: true,
- [CrepeFeature.CodeBlock]: true,
- [CrepeFeature.Table]: true,
- [CrepeFeature.Toolbar]: true,
- },
- });
-
- crepe.create().then(() => {
- console.log('Milkdown editor created');
-
- // Set up content change tracking
- const editorElement = editorRef.current?.querySelector('.ProseMirror');
- if (editorElement) {
- // Listen for input events on the editor
- const handleInput = () => {
- // Get current markdown content
- const markdown = crepe.getMarkdown();
- console.log('Editor content changed via input:', markdown.substring(0, 50) + '...');
- setCurrentContent(markdown);
-
- // Compare trimmed content to avoid whitespace issues
- const hasUnsavedChanges = markdown.trim() !== originalContent.trim();
- setHasChanges(hasUnsavedChanges);
- setIsReverted(false);
- };
-
- // Listen to multiple events to catch all changes
- editorElement.addEventListener('input', handleInput);
- editorElement.addEventListener('keyup', handleInput);
- editorElement.addEventListener('paste', handleInput);
- editorElement.addEventListener('cut', handleInput);
-
- // Store the handlers for cleanup
- (editorElement as any)._milkdownHandlers = {
- input: handleInput,
- keyup: handleInput,
- paste: handleInput,
- cut: handleInput
- };
- }
- }).catch((error) => {
- console.error('Failed to create Milkdown editor:', error);
- });
-
- crepeRef.current = crepe;
-
- return () => {
- // Clean up event listeners
- const editorElement = editorRef.current?.querySelector('.ProseMirror');
- if (editorElement && (editorElement as any)._milkdownHandlers) {
- const handlers = (editorElement as any)._milkdownHandlers;
- editorElement.removeEventListener('input', handlers.input);
- editorElement.removeEventListener('keyup', handlers.keyup);
- editorElement.removeEventListener('paste', handlers.paste);
- editorElement.removeEventListener('cut', handlers.cut);
- delete (editorElement as any)._milkdownHandlers;
- }
-
- if (crepeRef.current) {
- crepeRef.current.destroy();
- crepeRef.current = null;
- }
- };
- }, [doc.id, originalContent]);
-
- // Update theme class when isDarkMode changes
- useEffect(() => {
- if (editorRef.current) {
- if (isDarkMode) {
- editorRef.current.classList.add('milkdown-theme-dark');
- } else {
- editorRef.current.classList.remove('milkdown-theme-dark');
- }
- }
- }, [isDarkMode]);
-
- // Add keyboard shortcut for saving
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.metaKey || e.ctrlKey) && e.key === 's') {
- e.preventDefault();
- if (hasChanges && !isLoading) {
- handleSave();
- }
- }
- };
-
- window.addEventListener('keydown', handleKeyDown);
- return () => {
- window.removeEventListener('keydown', handleKeyDown);
- };
- }, [hasChanges, isLoading, currentContent]);
-
- // Handle manual save
- const handleSave = async () => {
- if (!hasChanges || isLoading) return;
-
- try {
- setIsLoading(true);
- console.log('Saving document with content:', currentContent.substring(0, 100) + '...');
-
- // Create updated document with markdown content stored in content field
- const updatedDocument = {
- ...doc,
- content: {
- markdown: currentContent,
- // Preserve any other content fields
- ...(typeof doc.content === 'object' && doc.content !== null ? doc.content : {})
- },
- updated_at: new Date().toISOString(),
- };
-
- await onSave(updatedDocument);
-
- // Update state after successful save
- setHasChanges(false);
- setIsReverted(false);
- setOriginalContent(currentContent);
- console.log('Document saved successfully');
- } catch (error) {
- console.error('Error saving document:', error);
- // You might want to show an error toast here
- } finally {
- setIsLoading(false);
- }
- };
-
- // Handle undo changes
- const handleUndo = () => {
- if (crepeRef.current && editorRef.current) {
- // Destroy and recreate editor with original content
- crepeRef.current.destroy();
-
- const crepe = new Crepe({
- root: editorRef.current,
- defaultValue: originalContent,
- features: {
- [CrepeFeature.HeaderMeta]: true,
- [CrepeFeature.LinkTooltip]: true,
- [CrepeFeature.ImageBlock]: true,
- [CrepeFeature.BlockEdit]: true,
- [CrepeFeature.ListItem]: true,
- [CrepeFeature.CodeBlock]: true,
- [CrepeFeature.Table]: true,
- [CrepeFeature.Toolbar]: true,
- },
- });
-
- crepe.create().then(() => {
- console.log('Milkdown editor reverted to original content');
-
- // Set up content change tracking for the new editor instance
- const editorElement = editorRef.current?.querySelector('.ProseMirror');
- if (editorElement) {
- const handleInput = () => {
- const markdown = crepe.getMarkdown();
- console.log('Editor content changed after undo:', markdown.substring(0, 50) + '...');
- setCurrentContent(markdown);
- const hasUnsavedChanges = markdown.trim() !== originalContent.trim();
- setHasChanges(hasUnsavedChanges);
- setIsReverted(false);
- };
-
- editorElement.addEventListener('input', handleInput);
- editorElement.addEventListener('keyup', handleInput);
- editorElement.addEventListener('paste', handleInput);
- editorElement.addEventListener('cut', handleInput);
-
- (editorElement as any)._milkdownHandlers = {
- input: handleInput,
- keyup: handleInput,
- paste: handleInput,
- cut: handleInput
- };
- }
-
- setCurrentContent(originalContent);
- setHasChanges(false);
- setIsReverted(true);
- }).catch((error) => {
- console.error('Failed to revert Milkdown editor:', error);
- });
-
- crepeRef.current = crepe;
- }
- };
-
- return (
-
-
-
-
- {doc.title}
-
-
- {isLoading ? (
-
-
- Saving...
-
- ) : isReverted ? (
-
-
- Reverted
-
- ) : hasChanges ? (
-
-
- Unsaved changes
-
- ) : (
-
-
- All changes saved
-
- )}
-
-
-
- {hasChanges && (
-
-
- Undo
-
- )}
-
-
- {isLoading ? 'Saving...' : 'Save'}
-
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/Tabs.tsx b/archon-ui-main/src/components/project-tasks/Tabs.tsx
deleted file mode 100644
index fd66d55ce5..0000000000
--- a/archon-ui-main/src/components/project-tasks/Tabs.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import React, { useMemo, useState, createContext, useContext } from 'react';
-interface TabsProps {
- defaultValue: string;
- value?: string;
- onValueChange?: (value: string) => void;
- children: React.ReactNode;
- className?: string;
-}
-const TabsContext = createContext<{
- value: string;
- onValueChange: (value: string) => void;
-}>({
- value: '',
- onValueChange: () => {}
-});
-export const Tabs = ({
- defaultValue,
- value,
- onValueChange,
- children,
- className = ''
-}: TabsProps) => {
- const [internalValue, setInternalValue] = useState(defaultValue);
- const activeValue = value !== undefined ? value : internalValue;
- const contextValue = useMemo(() => ({
- value: activeValue,
- onValueChange: (newValue: string) => {
- setInternalValue(newValue);
- onValueChange?.(newValue);
- }
- }), [activeValue, onValueChange]);
- return
- {children}
- ;
-};
-interface TabsListProps {
- children: React.ReactNode;
- className?: string;
-}
-export const TabsList = ({
- children,
- className = ''
-}: TabsListProps) => {
- return
- {/* Subtle neon glow effect */}
-
- {children}
-
;
-};
-interface TabsTriggerProps {
- value: string;
- children: React.ReactNode;
- className?: string;
- onClick?: () => void;
- color?: 'blue' | 'purple' | 'pink' | 'orange' | 'cyan' | 'green';
-}
-export const TabsTrigger = ({
- value,
- children,
- className = '',
- onClick,
- color = 'blue'
-}: TabsTriggerProps) => {
- const {
- value: activeValue,
- onValueChange
- } = useContext(TabsContext);
- const isActive = activeValue === value;
- const handleClick = () => {
- onValueChange(value);
- onClick?.();
- };
- const colorMap = {
- blue: {
- text: 'text-blue-600 dark:text-blue-400',
- glow: 'bg-blue-500 shadow-[0_0_10px_2px_rgba(59,130,246,0.4)] dark:shadow-[0_0_20px_5px_rgba(59,130,246,0.7)]',
- hover: 'hover:text-blue-500 dark:hover:text-blue-400/70'
- },
- purple: {
- text: 'text-purple-600 dark:text-purple-400',
- glow: 'bg-purple-500 shadow-[0_0_10px_2px_rgba(168,85,247,0.4)] dark:shadow-[0_0_20px_5px_rgba(168,85,247,0.7)]',
- hover: 'hover:text-purple-500 dark:hover:text-purple-400/70'
- },
- pink: {
- text: 'text-pink-600 dark:text-pink-400',
- glow: 'bg-pink-500 shadow-[0_0_10px_2px_rgba(236,72,153,0.4)] dark:shadow-[0_0_20px_5px_rgba(236,72,153,0.7)]',
- hover: 'hover:text-pink-500 dark:hover:text-pink-400/70'
- },
- orange: {
- text: 'text-orange-600 dark:text-orange-400',
- glow: 'bg-orange-500 shadow-[0_0_10px_2px_rgba(249,115,22,0.4)] dark:shadow-[0_0_20px_5px_rgba(249,115,22,0.7)]',
- hover: 'hover:text-orange-500 dark:hover:text-orange-400/70'
- },
- cyan: {
- text: 'text-cyan-600 dark:text-cyan-400',
- glow: 'bg-cyan-500 shadow-[0_0_10px_2px_rgba(34,211,238,0.4)] dark:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]',
- hover: 'hover:text-cyan-500 dark:hover:text-cyan-400/70'
- },
- green: {
- text: 'text-emerald-600 dark:text-emerald-400',
- glow: 'bg-emerald-500 shadow-[0_0_10px_2px_rgba(16,185,129,0.4)] dark:shadow-[0_0_20px_5px_rgba(16,185,129,0.7)]',
- hover: 'hover:text-emerald-500 dark:hover:text-emerald-400/70'
- }
- };
- return
- {children}
- {/* Active state neon indicator */}
- {isActive && <>
-
- >}
- ;
-};
-interface TabsContentProps {
- value: string;
- children: React.ReactNode;
- className?: string;
-}
-export const TabsContent = ({
- value,
- children,
- className = ''
-}: TabsContentProps) => {
- const {
- value: activeValue
- } = useContext(TabsContext);
- // Simplified TabsContent - we're handling animations in the parent component now
- if (activeValue !== value) return null;
- return
- {children}
-
;
-};
\ 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
deleted file mode 100644
index 58761486bc..0000000000
--- a/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx
+++ /dev/null
@@ -1,371 +0,0 @@
-import React, { useRef, useState, useCallback } from 'react';
-import { useDrop } from 'react-dnd';
-import { useToast } from '../../contexts/ToastContext';
-import { DeleteConfirmModal } from '../common/DeleteConfirmModal';
-import { Trash2 } from 'lucide-react';
-import { Task } from './TaskTableView'; // Import Task interface
-import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
-import { DraggableTaskCard } from './DraggableTaskCard';
-
-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;
-}
-
-interface ColumnDropZoneProps {
- status: Task['status'];
- title: string;
- tasks: Task[];
- onTaskMove: (taskId: string, newStatus: Task['status']) => void;
- onTaskView: (task: Task) => void;
- onTaskDelete: (task: Task) => void;
- onTaskReorder: (taskId: string, targetIndex: number, status: Task['status']) => void;
- allTasks: Task[];
- hoveredTaskId: string | null;
- onTaskHover: (taskId: string | null) => void;
- selectedTasks: Set;
- onTaskSelect: (taskId: string) => void;
-}
-
-const ColumnDropZone = ({
- status,
- title,
- tasks,
- onTaskMove,
- onTaskView,
- onTaskDelete,
- onTaskReorder,
- allTasks,
- hoveredTaskId,
- onTaskHover,
- selectedTasks,
- onTaskSelect
-}: ColumnDropZoneProps) => {
- const ref = useRef(null);
-
- const [{ isOver }, drop] = useDrop({
- accept: ItemTypes.TASK,
- drop: (item: { id: string; status: Task['status'] }) => {
- 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 'todo':
- return 'text-gray-600 dark:text-gray-400';
- case 'doing':
- return 'text-blue-600 dark:text-blue-400';
- case 'review':
- return 'text-purple-600 dark:text-purple-400';
- case 'done':
- return 'text-green-600 dark:text-green-400';
- }
- };
-
- // Get column header glow based on status
- const getColumnGlow = () => {
- switch (status) {
- case 'todo':
- return 'bg-gray-500/30';
- case 'doing':
- 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 'done':
- 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}
- hoveredTaskId={hoveredTaskId}
- onTaskHover={onTaskHover}
- selectedTasks={selectedTasks}
- onTaskSelect={onTaskSelect}
- />
- ))}
-
-
- );
-};
-
-export const TaskBoardView = ({
- tasks,
- onTaskView,
- onTaskComplete,
- onTaskDelete,
- onTaskMove,
- onTaskReorder
-}: 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();
-
- // 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 {
- const results = await Promise.allSettled(
- tasksToDelete.map(task => Promise.resolve(onTaskDelete(task)))
- );
- const rejected = results.filter(r => r.status === 'rejected');
- if (rejected.length) {
- console.error('Some deletions failed:', rejected.length);
- }
- clearSelection();
- } catch (error) {
- console.error('Failed to delete tasks:', error);
- }
- }, [selectedTasks, tasks, onTaskDelete, clearSelection]);
-
- // 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 {
- // Call onTaskMove so optimistic UI and counts refresh immediately; parent persists
- tasksToUpdate.forEach(task => onTaskMove(task.id, newStatus));
- clearSelection();
- showToast(`Moved ${tasksToUpdate.length} task${tasksToUpdate.length !== 1 ? 's' : ''} to ${newStatus}`, 'success');
- } catch (error) {
- console.error('Failed to update tasks:', error);
- showToast('Failed to move some tasks', 'error');
- }
- }, [selectedTasks, tasks, onTaskMove, clearSelection, showToast]);
-
- // No status mapping needed - using database values directly
-
- // 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 {
- // Delegate deletion to parent to avoid duplicate API calls
- await onTaskDelete(taskToDelete);
- } catch (error) {
- console.error('Failed to delete task:', error);
- } finally {
- setShowDeleteConfirm(false);
- setTaskToDelete(null);
- }
- }, [taskToDelete, onTaskDelete, setShowDeleteConfirm, setTaskToDelete]);
-
- // 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);
- };
-
- // Note: With optimistic updates, we no longer show loading overlays for successful moves
- // Tasks update instantly and only rollback on actual failures
-
- return (
-
-
- {/* Multi-select toolbar */}
- {selectedTasks.size > 0 && (
-
-
-
- {selectedTasks.size} task{selectedTasks.size !== 1 ? 's' : ''} selected
-
-
-
-
- {/* Status change dropdown */}
- {
- if (e.target.value) {
- handleMassStatusChange(e.target.value as Task['status']);
- e.target.value = ''; // Reset dropdown
- }
- }}
- defaultValue=""
- >
- Move to...
- Todo
- Doing
- Review
- Done
-
-
- {/* Mass delete button */}
-
-
- Delete
-
-
- {/* Clear selection */}
-
- Clear
-
-
-
- )}
-
- {/* Board Columns */}
-
- {/* Todo Column */}
-
-
- {/* Doing Column */}
-
-
- {/* Review Column */}
-
-
- {/* Done Column */}
-
-
-
- {/* Delete Confirmation Modal for Tasks */}
- {showDeleteConfirm && taskToDelete && (
-
- )}
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx b/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx
deleted file mode 100644
index 75924c33f9..0000000000
--- a/archon-ui-main/src/components/project-tasks/TaskInputComponents.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import React, { memo, useState, useEffect, useCallback, useRef } from 'react';
-
-interface DebouncedInputProps {
- value: string;
- onChange: (value: string) => void;
- placeholder?: string;
- className?: string;
- type?: 'text' | 'textarea';
- rows?: number;
-}
-
-// Memoized input component that manages its own state
-export const DebouncedInput = memo(({
- value,
- onChange,
- placeholder,
- className,
- type = 'text',
- rows = 5
-}: DebouncedInputProps) => {
- const [localValue, setLocalValue] = useState(value);
- const timeoutRef = useRef();
- const isFirstRender = useRef(true);
-
- // Sync with external value only on mount or when externally changed
- useEffect(() => {
- if (isFirstRender.current) {
- isFirstRender.current = false;
- return;
- }
- // Only update if the external value is different from local
- if (value !== localValue) {
- setLocalValue(value);
- }
- }, [value]);
-
- const handleChange = useCallback((e: React.ChangeEvent) => {
- const newValue = e.target.value;
- setLocalValue(newValue);
-
- // Clear existing timeout
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
-
- // Set new timeout for debounced update
- timeoutRef.current = setTimeout(() => {
- onChange(newValue);
- }, 300);
- }, [onChange]);
-
- // Cleanup timeout on unmount
- useEffect(() => {
- return () => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
- };
- }, []);
-
- if (type === 'textarea') {
- return (
-
- );
- }
-
- return (
-
- );
-}, (prevProps, nextProps) => {
- // Custom comparison - only re-render if external value or onChange changes
- return prevProps.value === nextProps.value &&
- prevProps.onChange === nextProps.onChange &&
- prevProps.placeholder === nextProps.placeholder &&
- prevProps.className === nextProps.className;
-});
-
-DebouncedInput.displayName = 'DebouncedInput';
-
-interface FeatureInputProps {
- value: string;
- onChange: (value: string) => void;
- projectFeatures: any[];
- isLoadingFeatures: boolean;
- placeholder?: string;
- className?: string;
-}
-
-// Memoized feature input with datalist
-export const FeatureInput = memo(({
- value,
- onChange,
- projectFeatures,
- isLoadingFeatures,
- placeholder,
- className
-}: FeatureInputProps) => {
- const [localValue, setLocalValue] = useState(value);
- const timeoutRef = useRef();
-
- // Sync with external value
- useEffect(() => {
- if (value !== localValue) {
- setLocalValue(value);
- }
- }, [value]);
-
- const handleChange = useCallback((e: React.ChangeEvent) => {
- const newValue = e.target.value;
- setLocalValue(newValue);
-
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
-
- timeoutRef.current = setTimeout(() => {
- onChange(newValue);
- }, 300);
- }, [onChange]);
-
- useEffect(() => {
- return () => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
- }
- };
- }, []);
-
- return (
-
-
-
- {projectFeatures.map((feature) => (
-
- {feature.label} ({feature.type})
-
- ))}
-
- {isLoadingFeatures && (
-
- )}
-
- );
-}, (prevProps, nextProps) => {
- return prevProps.value === nextProps.value &&
- prevProps.onChange === nextProps.onChange &&
- prevProps.isLoadingFeatures === nextProps.isLoadingFeatures &&
- prevProps.projectFeatures === nextProps.projectFeatures;
-});
-
-FeatureInput.displayName = 'FeatureInput';
\ 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
deleted file mode 100644
index e830e0eac2..0000000000
--- a/archon-ui-main/src/components/project-tasks/TaskTableView.tsx
+++ /dev/null
@@ -1,875 +0,0 @@
-import React, { useState, useCallback, useRef, useEffect } from 'react';
-import { useDrag, useDrop } from 'react-dnd';
-import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus } from 'lucide-react';
-import { useToast } from '../../contexts/ToastContext';
-import { DeleteConfirmModal } from '../common/DeleteConfirmModal';
-import { projectService } from '../../services/projectService';
-import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils';
-import { DraggableTaskCard } from './DraggableTaskCard';
-
-export interface Task {
- id: string;
- title: string;
- description: string;
- status: 'todo' | 'doing' | 'review' | 'done';
- 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;
-}
-
-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' ? (
- {
- setEditValue(e.target.value);
- // Auto-save on select change
- setTimeout(() => handleSave(), 0);
- }}
- onKeyDown={handleKeyDown}
- onBlur={handleBlur}
- className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
- autoFocus
- >
- {options.map(option => (
- {option}
- ))}
-
- ) : type === 'textarea' ? (
-
- );
-};
-
-interface DraggableTaskRowProps {
- task: Task;
- index: number;
- onTaskView: (task: Task) => void;
- onTaskComplete: (taskId: string) => void;
- onTaskDelete: (task: Task) => void;
- onTaskReorder: (taskId: string, newOrder: number, status: Task['status']) => void;
- onTaskUpdate?: (taskId: string, updates: Partial) => Promise;
- tasksInStatus: Task[];
- style?: React.CSSProperties;
-}
-
-const DraggableTaskRow = ({
- task,
- index,
- onTaskView,
- onTaskComplete,
- onTaskDelete,
- onTaskReorder,
- onTaskUpdate,
- tasksInStatus,
- style
-}: DraggableTaskRowProps) => {
- const [editingField, setEditingField] = useState(null);
- const [isHovering, setIsHovering] = useState(false);
-
- const [{ isDragging }, drag] = useDrag({
- type: ItemTypes.TASK,
- item: { id: task.id, index, status: task.status },
- collect: (monitor) => ({
- isDragging: !!monitor.isDragging(),
- }),
- });
-
- const [{ isOver, canDrop }, drop] = useDrop({
- accept: ItemTypes.TASK,
- hover: (draggedItem: { id: string; index: number; status: Task['status'] }, 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('HOVER: Moving task', draggedItem.id, 'to index', draggedIndex, 'to', hoveredIndex);
-
- // Move the task immediately for visual feedback
- onTaskReorder(draggedItem.id, hoveredIndex, task.status);
-
- // Update the dragged item's index to prevent re-triggering
- draggedItem.index = hoveredIndex;
- },
- collect: (monitor) => ({
- isOver: !!monitor.isOver(),
- canDrop: !!monitor.canDrop(),
- }),
- });
-
- const handleUpdateField = async (field: string, value: string) => {
- if (onTaskUpdate) {
- const updates: Partial = {};
-
- if (field === 'title') {
- updates.title = value;
- } else if (field === 'status') {
- updates.status = value as Task['status'];
- } else if (field === 'assignee') {
- updates.assignee = { name: value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' };
- } else if (field === 'feature') {
- updates.feature = value;
- }
-
- try {
- await onTaskUpdate(task.id, updates);
- setEditingField(null);
- } catch (error) {
- console.error('Failed to update task:', error);
- }
- }
- };
-
- return (
- drag(drop(node))}
- className={`
- group transition-all duration-200 cursor-move
- ${index % 2 === 0 ? 'bg-white/50 dark:bg-black/50' : 'bg-gray-50/80 dark:bg-gray-900/30'}
- hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70 dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20
- border-b border-gray-200 dark:border-gray-800 last:border-b-0
- ${isDragging ? 'opacity-50 scale-105 shadow-lg z-50' : ''}
- ${isOver && canDrop ? 'bg-cyan-100/50 dark:bg-cyan-900/20 border-cyan-400' : ''}
- ${isHovering ? 'transform translate-y-1 shadow-md' : ''}
- `}
- onMouseLeave={() => setIsHovering(false)}
- style={style}
- >
-
-
-
- {task.task_order}
-
-
-
-
-
-
- handleUpdateField('title', value)}
- isEditing={editingField === 'title'}
- onEdit={() => setEditingField('title')}
- onCancel={() => setEditingField(null)}
- placeholder="Task title..."
- />
-
-
-
-
- {
- handleUpdateField('status', value as Task['status']);
- }}
- type="select"
- options={['todo', 'doing', 'review', 'done']}
- isEditing={editingField === 'status'}
- onEdit={() => setEditingField('status')}
- onCancel={() => setEditingField(null)}
- />
-
-
-
- handleUpdateField('feature', value)}
- isEditing={editingField === 'feature'}
- onEdit={() => setEditingField('feature')}
- onCancel={() => setEditingField(null)}
- placeholder="Feature name..."
- />
-
-
-
-
-
setEditingField('assignee')}
- title={`Assignee: ${task.assignee?.name || 'User'}`}
- >
- {getAssigneeIcon(task.assignee?.name || 'User')}
-
- {editingField === 'assignee' && (
-
- {
- handleUpdateField('assignee', e.target.value);
- setEditingField(null);
- }}
- className="bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-cyan-500"
- autoFocus
- >
- User
- Archon
- AI IDE Agent
-
-
- )}
-
-
-
-
-
onTaskDelete(task)}
- className="p-1.5 rounded-full bg-red-500/20 text-red-500 hover:bg-red-500/30 hover:shadow-[0_0_10px_rgba(239,68,68,0.3)] transition-all duration-300"
- title="Delete task"
- aria-label="Delete task"
- >
-
-
-
onTaskComplete(task.id)}
- className="p-1.5 rounded-full bg-green-500/20 text-green-500 hover:bg-green-500/30 hover:shadow-[0_0_10px_rgba(34,197,94,0.3)] transition-all duration-300"
- title="Mark task as complete"
- aria-label="Mark task as complete"
- >
-
-
-
onTaskView(task)}
- className="p-1.5 rounded-full bg-cyan-500/20 text-cyan-500 hover:bg-cyan-500/30 hover:shadow-[0_0_10px_rgba(34,211,238,0.3)] transition-all duration-300"
- title="Edit task"
- aria-label="Edit task"
- >
-
-
- {/* Copy Task ID Button - Matching Board View */}
-
{
- e.stopPropagation();
- navigator.clipboard.writeText(task.id);
- // Visual feedback like in board view
- const button = e.currentTarget;
- const originalHTML = button.innerHTML;
- button.innerHTML = '✓ Copied
';
- setTimeout(() => {
- button.innerHTML = originalHTML;
- }, 2000);
- }}
- className="p-1.5 rounded-full bg-gray-500/20 text-gray-500 hover:bg-gray-500/30 hover:shadow-[0_0_10px_rgba(107,114,128,0.3)] transition-all duration-300"
- title="Copy Task ID to clipboard"
- aria-label="Copy Task ID to clipboard"
- >
-
-
-
-
-
- );
-};
-
-// Add Task Row Component - Always visible empty input row
-interface AddTaskRowProps {
- onTaskCreate?: (task: Omit) => Promise;
- tasks: Task[];
- statusFilter: Task['status'] | 'all';
-}
-
-const AddTaskRow = ({ onTaskCreate, tasks, statusFilter }: AddTaskRowProps) => {
- const [newTask, setNewTask] = useState>({
- title: '',
- description: '',
- status: statusFilter === 'all' ? 'todo' : statusFilter,
- assignee: { name: 'AI IDE Agent', avatar: '' },
- feature: '',
- featureColor: '#3b82f6',
- task_order: 1
- });
-
- const handleCreateTask = async () => {
- if (!newTask.title.trim() || !onTaskCreate) return;
-
- // Calculate the next order number for the target status
- const targetStatus = newTask.status;
- const tasksInStatus = tasks.filter(t => t.status === targetStatus);
- const nextOrder = tasksInStatus.length > 0 ? Math.max(...tasksInStatus.map(t => t.task_order)) + 1 : 1;
-
- try {
- await onTaskCreate({
- ...newTask,
- task_order: nextOrder
- });
-
- // Reset only the title to allow quick adding
- setNewTask(prev => ({
- ...prev,
- title: '',
- description: ''
- }));
- } catch (error) {
- console.error('Failed to create task:', error);
- }
- };
-
- const handleKeyPress = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- handleCreateTask();
- }
- };
-
- // Update status when filter changes
- React.useEffect(() => {
- if (statusFilter !== 'all') {
- setNewTask(prev => ({ ...prev, status: statusFilter }));
- }
- }, [statusFilter]);
-
- return (
- <>
-
- {/* Toned down neon blue line separator */}
-
-
-
-
-
-
-
-
-
- setNewTask(prev => ({ ...prev, title: e.target.value }))}
- onKeyPress={handleKeyPress}
- placeholder="Type task title and press Enter..."
- className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)] transition-all duration-200"
- autoFocus
- />
-
-
- {
- setNewTask(prev => ({ ...prev, status: e.target.value as Task['status'] }));
- }}
- className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
- >
- Todo
- Doing
- Review
- Done
-
-
-
- setNewTask(prev => ({ ...prev, feature: e.target.value }))}
- onKeyPress={handleKeyPress}
- placeholder="Feature..."
- className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
- />
-
-
- setNewTask(prev => ({
- ...prev,
- assignee: { name: e.target.value as 'User' | 'Archon' | 'AI IDE Agent', avatar: '' }
- }))}
- className="w-full bg-white/90 dark:bg-black/90 border border-cyan-300 dark:border-cyan-600 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_5px_rgba(34,211,238,0.3)]"
- >
- AI IDE Agent
- User
- Archon
-
-
-
-
- Press Enter
-
-
-
- >
- );
-};
-
-export const TaskTableView = ({
- tasks,
- onTaskView,
- onTaskComplete,
- onTaskDelete,
- onTaskReorder,
- onTaskCreate,
- onTaskUpdate
-}: TaskTableViewProps) => {
- const [statusFilter, setStatusFilter] = useState('todo');
-
- // State for delete confirmation modal
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [taskToDelete, setTaskToDelete] = useState(null);
-
- const { showToast } = useToast();
-
- // Refs for scroll fade effect
- const tableContainerRef = useRef(null);
- const tableRef = useRef(null);
- const [scrollOpacities, setScrollOpacities] = useState>(new Map());
-
- // Calculate opacity based on row position
- const calculateOpacity = (rowElement: HTMLElement, containerElement: HTMLElement) => {
- const containerRect = containerElement.getBoundingClientRect();
- const rowRect = rowElement.getBoundingClientRect();
-
- // Calculate the row's position relative to the container
- const rowCenter = rowRect.top + rowRect.height / 2;
- const containerCenter = containerRect.top + containerRect.height / 2;
- const containerHeight = containerRect.height;
-
- // Distance from center (0 at center, 1 at edges)
- const distanceFromCenter = Math.abs(rowCenter - containerCenter) / (containerHeight / 2);
-
- // Create a smooth fade effect
- // Rows at the top 40% of viewport get full opacity
- // Rows fade out smoothly towards the bottom
- const relativePosition = (rowRect.top - containerRect.top) / containerHeight;
-
- if (relativePosition < 0) {
- return 1; // Full opacity for rows above viewport
- } else if (relativePosition < 0.4) {
- return 1; // Full opacity for top 40%
- } else if (relativePosition > 0.9) {
- return 0.15; // Very faded at bottom (slightly more visible)
- } else {
- // Smooth transition from 1 to 0.15
- const fadeRange = 0.9 - 0.4; // 0.5
- const fadePosition = (relativePosition - 0.4) / fadeRange;
- return 1 - (fadePosition * 0.85); // Fade from 1 to 0.15
- }
- };
-
- // Update opacities on scroll
- const updateOpacities = useCallback(() => {
- if (!tableContainerRef.current || !tableRef.current) return;
-
- const container = tableContainerRef.current;
- const rows = tableRef.current.querySelectorAll('tbody tr');
- const newOpacities = new Map();
-
- rows.forEach((row, index) => {
- const opacity = calculateOpacity(row as HTMLElement, container);
- newOpacities.set(`row-${index}`, opacity);
- });
-
- setScrollOpacities(newOpacities);
- }, []);
-
- // Set up scroll listener
- useEffect(() => {
- const container = tableContainerRef.current;
- if (!container) return;
-
- // Initial opacity calculation
- updateOpacities();
-
- // Update on scroll
- container.addEventListener('scroll', updateOpacities);
-
- // Also update on window resize
- window.addEventListener('resize', updateOpacities);
-
- return () => {
- container.removeEventListener('scroll', updateOpacities);
- window.removeEventListener('resize', updateOpacities);
- };
- }, [updateOpacities, tasks]); // Re-calculate when tasks change
-
- // 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); // Call backend service
- onTaskDelete(taskToDelete); // Notify parent component
- 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]);
-
- // Group tasks by status and sort by task_order
- const getTasksByStatus = (status: Task['status']) => {
- return tasks
- .filter(task => task.status === status)
- .sort((a, b) => a.task_order - b.task_order);
- };
-
- // Simply return tasks as-is (no hierarchy)
- const organizeTasksHierarchically = (taskList: Task[]) => {
- return taskList;
- };
-
- // Apply status filtering
- let statusFilteredTasks: Task[];
-
- if (statusFilter === 'all') {
- statusFilteredTasks = tasks;
- } else {
- statusFilteredTasks = tasks.filter(task => task.status === statusFilter);
- }
-
- const filteredTasks = organizeTasksHierarchically(statusFilteredTasks);
-
- const statuses: Task['status'][] = ['todo', 'doing', 'review', 'done'];
-
- // Get column header color and glow based on header type (matching board view style)
- const getHeaderColor = (type: 'primary' | 'secondary') => {
- return type === 'primary' ? 'text-cyan-600 dark:text-cyan-400' : 'text-purple-600 dark:text-purple-400';
- };
-
- const getHeaderGlow = (type: 'primary' | 'secondary') => {
- return type === 'primary' ? 'bg-cyan-500 shadow-[0_0_8px_rgba(34,211,238,0.6)]' : 'bg-purple-500 shadow-[0_0_8px_rgba(168,85,247,0.6)]';
- };
-
- return (
-
- {/* Status Filter */}
-
-
- setStatusFilter('all')}
- className={`
- px-3 py-1.5 rounded-full text-xs transition-all duration-200
- ${statusFilter === 'all'
- ? 'bg-cyan-100 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400 ring-1 ring-cyan-500/50 shadow-[0_0_8px_rgba(34,211,238,0.3)]'
- : 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-gray-200/70 dark:hover:bg-gray-700/50'
- }
- `}
- >
- All Tasks
-
- {statuses.map((status) => {
- // Define colors for each status
- const getStatusColors = (status: Task['status']) => {
- switch (status) {
- case 'todo':
- return {
- selected: 'bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 ring-1 ring-gray-500/50 shadow-[0_0_8px_rgba(107,114,128,0.3)]',
- unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-gray-200/70 dark:hover:bg-gray-700/50'
- };
- case 'doing':
- return {
- selected: 'bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 ring-1 ring-blue-500/50 shadow-[0_0_8px_rgba(59,130,246,0.3)]',
- unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-blue-200/30 dark:hover:bg-blue-900/20'
- };
- case 'review':
- return {
- selected: 'bg-purple-100 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 ring-1 ring-purple-500/50 shadow-[0_0_8px_rgba(168,85,247,0.3)]',
- unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-purple-200/30 dark:hover:bg-purple-900/20'
- };
- case 'done':
- return {
- selected: 'bg-green-100 dark:bg-green-900/20 text-green-600 dark:text-green-400 ring-1 ring-green-500/50 shadow-[0_0_8px_rgba(34,197,94,0.3)]',
- unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-green-200/30 dark:hover:bg-green-900/20'
- };
- default:
- return {
- selected: 'bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 ring-1 ring-gray-500/50 shadow-[0_0_8px_rgba(107,114,128,0.3)]',
- unselected: 'bg-gray-100/70 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 hover:bg-gray-200/70 dark:hover:bg-gray-700/50'
- };
- }
- };
-
- const colors = getStatusColors(status);
-
- return (
- setStatusFilter(status)}
- className={`
- px-3 py-1.5 rounded-full text-xs transition-all duration-200
- ${statusFilter === status ? colors.selected : colors.unselected}
- `}
- >
- {status === 'todo' ? 'Todo' :
- status === 'doing' ? 'Doing' :
- status === 'review' ? 'Review' : 'Done'}
-
- );
- })}
-
-
-
- {/* Scrollable table container */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Order
-
-
- {/* Header divider with glow matching board view */}
-
-
-
-
- Task
-
-
-
-
-
-
- Status
-
-
-
-
-
-
- Feature
-
-
-
-
-
-
- Assignee
-
-
-
-
-
-
- Actions
-
-
-
-
-
-
-
- {filteredTasks.map((task, index) => (
-
- ))}
- {/* Add Task Row - only show if create permission exists */}
- {onTaskCreate && (
-
- )}
-
-
- {/* Spacer to allow scrolling last rows to top */}
-
-
-
- {/* Delete Confirmation Modal for Tasks */}
- {showDeleteConfirm && taskToDelete && (
-
- )}
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/TasksTab.tsx b/archon-ui-main/src/components/project-tasks/TasksTab.tsx
deleted file mode 100644
index dff0d58f9c..0000000000
--- a/archon-ui-main/src/components/project-tasks/TasksTab.tsx
+++ /dev/null
@@ -1,529 +0,0 @@
-import React, { useState, useEffect, useCallback, useMemo } from 'react';
-import { Table, LayoutGrid, Plus } from 'lucide-react';
-import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
-import { Toggle } from '../ui/Toggle';
-import { projectService } from '../../services/projectService';
-import { useToast } from '../../contexts/ToastContext';
-import { debounce } from '../../utils/debounce';
-import { calculateReorderPosition, getDefaultTaskOrder } from '../../utils/taskOrdering';
-
-import type { CreateTaskRequest, UpdateTaskRequest } from '../../types/project';
-import { TaskTableView, Task } from './TaskTableView';
-import { TaskBoardView } from './TaskBoardView';
-import { EditTaskModal } from './EditTaskModal';
-
-// Type for optimistic task updates with operation tracking
-type OptimisticTask = Task & { _optimisticOperationId: string };
-
-
-
-export const TasksTab = ({
- initialTasks,
- onTasksChange,
- projectId
-}: {
- initialTasks: Task[];
- onTasksChange: (tasks: Task[]) => void;
- projectId: string;
-}) => {
- const { showToast } = useToast();
- const [viewMode, setViewMode] = useState<'table' | 'board'>('board');
- const [tasks, setTasks] = useState([]);
- const [editingTask, setEditingTask] = useState(null);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [projectFeatures, setProjectFeatures] = useState([]);
- const [isLoadingFeatures, setIsLoadingFeatures] = useState(false);
- const [isSavingTask, setIsSavingTask] = useState(false);
- const [optimisticTaskUpdates, setOptimisticTaskUpdates] = useState>(new Map());
-
- // Initialize tasks, but preserve optimistic updates
- useEffect(() => {
- if (optimisticTaskUpdates.size === 0) {
- // No optimistic updates, use incoming data as-is
- setTasks(initialTasks);
- } else {
- // Merge incoming data with optimistic updates
- const mergedTasks = initialTasks.map(task => {
- const optimisticUpdate = optimisticTaskUpdates.get(task.id);
- if (optimisticUpdate) {
- console.log(`[TasksTab] Preserving optimistic update for task ${task.id}:`, optimisticUpdate.status);
- // Clean up internal tracking field before returning
- const { _optimisticOperationId, ...cleanTask } = optimisticUpdate;
- return cleanTask as Task; // Keep optimistic version without internal fields
- }
- return task; // Use polling data for non-optimistic tasks
- });
- setTasks(mergedTasks);
- }
- }, [initialTasks, optimisticTaskUpdates]);
-
- // Load project features on component mount
- useEffect(() => {
- loadProjectFeatures();
- }, [projectId]);
-
-
- const loadProjectFeatures = async () => {
- if (!projectId) return;
-
- setIsLoadingFeatures(true);
- try {
- const response = await projectService.getProjectFeatures(projectId);
- setProjectFeatures(response.features || []);
- } catch (error) {
- console.error('Failed to load project features:', error);
- setProjectFeatures([]);
- } finally {
- setIsLoadingFeatures(false);
- }
- };
-
- // Modal management functions
- const openEditModal = async (task: Task) => {
- setEditingTask(task);
- setIsModalOpen(true);
- };
-
- const closeModal = () => {
- setIsModalOpen(false);
- setEditingTask(null);
- };
-
- const saveTask = async (task: Task) => {
- setEditingTask(task);
-
- setIsSavingTask(true);
- try {
-
- if (task.id) {
- // Update existing task
- const updateData: UpdateTaskRequest = {
- title: task.title,
- description: task.description,
- status: task.status,
- assignee: task.assignee?.name || 'User',
- task_order: task.task_order,
- ...(task.feature && { feature: task.feature }),
- ...(task.featureColor && { featureColor: task.featureColor })
- };
-
- await projectService.updateTask(task.id, updateData);
- } else {
- // Create new task first to get UUID
- const createData: CreateTaskRequest = {
- project_id: projectId,
- title: task.title,
- description: task.description,
- status: task.status,
- assignee: task.assignee?.name || 'User',
- task_order: task.task_order,
- ...(task.feature && { feature: task.feature }),
- ...(task.featureColor && { featureColor: task.featureColor })
- };
-
- await projectService.createTask(createData);
- }
-
- // Task saved - polling will pick up changes automatically
- closeModal();
- } catch (error) {
- console.error('Failed to save task:', error);
- showToast(`Failed to save task: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error');
- } finally {
- setIsSavingTask(false);
- }
- };
-
- // Update tasks helper
- const updateTasks = (newTasks: Task[]) => {
- setTasks(newTasks);
- onTasksChange(newTasks);
- };
-
-
- // Helper function to get next available order number for a status
- const getNextOrderForStatus = (status: Task['status']): number => {
- const tasksInStatus = tasks.filter(task =>
- task.status === status
- );
-
- if (tasksInStatus.length === 0) return 1;
-
- const maxOrder = Math.max(...tasksInStatus.map(task => task.task_order));
- return maxOrder + 1;
- };
-
- // Use shared debounce helper
-
- // Improved debounced persistence with better coordination
- const debouncedPersistSingleTask = useMemo(
- () => debounce(async (task: Task) => {
- try {
- console.log('REORDER: Persisting position change for task:', task.title, 'new position:', task.task_order);
-
- // Update only the moved task with server timestamp for conflict resolution
- await projectService.updateTask(task.id, {
- task_order: task.task_order,
- client_timestamp: Date.now()
- });
- console.log('REORDER: Single task position persisted successfully');
-
- } catch (error) {
- console.error('REORDER: Failed to persist task position:', error);
- // Polling will eventually sync the correct state
- }
- }, 800), // Slightly reduced delay for better responsiveness
- []
- );
-
- // Optimized task reordering without optimistic update conflicts
- const handleTaskReorder = useCallback((taskId: string, targetIndex: number, status: Task['status']) => {
- console.log('REORDER: Moving task', taskId, 'to index', targetIndex, 'in status', status);
-
- // Get all tasks in the target status, sorted by current order
- const statusTasks = tasks
- .filter(task => task.status === status)
- .sort((a, b) => a.task_order - b.task_order);
-
- const otherTasks = tasks.filter(task => task.status !== status);
-
- // Find the moving task
- const movingTaskIndex = statusTasks.findIndex(task => task.id === taskId);
- if (movingTaskIndex === -1) {
- console.log('REORDER: Task not found in status');
- return;
- }
-
- // Prevent invalid moves
- if (targetIndex < 0 || targetIndex >= statusTasks.length) {
- console.log('REORDER: Invalid target index', targetIndex);
- return;
- }
-
- // Skip if moving to same position
- if (movingTaskIndex === targetIndex) {
- console.log('REORDER: Task already in target position');
- return;
- }
-
- const movingTask = statusTasks[movingTaskIndex];
- console.log('REORDER: Moving', movingTask.title, 'from', movingTaskIndex, 'to', targetIndex);
-
- // Calculate new position using shared ordering utility
- const newPosition = calculateReorderPosition(statusTasks, movingTaskIndex, targetIndex);
-
- console.log('REORDER: New position calculated:', newPosition);
-
- // Create updated task with new position
- const updatedTask = {
- ...movingTask,
- task_order: newPosition
- };
-
- // Immediate UI update without optimistic tracking interference
- const allUpdatedTasks = otherTasks.concat(
- statusTasks.map(task => task.id === taskId ? updatedTask : task)
- );
- updateTasks(allUpdatedTasks);
-
- // Persist to backend (single API call)
- debouncedPersistSingleTask(updatedTask);
- }, [tasks, updateTasks, debouncedPersistSingleTask]);
-
- // Task move function (for board view) - Optimistic Updates with Concurrent Operation Protection
- const moveTask = async (taskId: string, newStatus: Task['status']) => {
- // Generate unique operation ID to handle concurrent operations
- const operationId = `${taskId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- console.log(`[TasksTab] Optimistically moving task ${taskId} to ${newStatus} (op: ${operationId})`);
-
- // Clear any previous errors (removed local error state)
-
- // Find the task and validate
- const movingTask = tasks.find(task => task.id === taskId);
- if (!movingTask) {
- showToast('Task not found', 'error');
- return;
- }
-
- // (pendingOperations removed)
-
- // 1. Save current state for rollback
- const previousTasks = [...tasks]; // Shallow clone sufficient
- const newOrder = getNextOrderForStatus(newStatus);
-
- // 2. Update UI immediately (optimistic update - no loader!)
- const optimisticTask: OptimisticTask = {
- ...movingTask,
- status: newStatus,
- task_order: newOrder,
- _optimisticOperationId: operationId // Track which operation created this
- };
- const optimisticTasks = tasks.map(task =>
- task.id === taskId ? optimisticTask : task
- );
-
- // Track this as an optimistic update with operation ID
- setOptimisticTaskUpdates(prev => new Map(prev).set(taskId, optimisticTask));
- updateTasks(optimisticTasks);
-
- // 3. Call API in background
- try {
- await projectService.updateTask(taskId, {
- status: newStatus,
- task_order: newOrder,
- client_timestamp: Date.now()
- });
-
- console.log(`[TasksTab] Successfully moved task ${taskId} (op: ${operationId})`);
-
- // Only clear if this is still the current operation (no newer operation started)
- setOptimisticTaskUpdates(prev => {
- const currentOptimistic = prev.get(taskId);
- if (currentOptimistic?._optimisticOperationId === operationId) {
- const newMap = new Map(prev);
- newMap.delete(taskId);
- return newMap;
- }
- return prev; // Don't clear, newer operation is active
- });
-
- } catch (error) {
- console.error(`[TasksTab] Failed to move task ${taskId} (op: ${operationId}):`, error);
-
- // Only rollback if this is still the current operation
- setOptimisticTaskUpdates(prev => {
- const currentOptimistic = prev.get(taskId);
- if (currentOptimistic?._optimisticOperationId === operationId) {
- // 4. Rollback on failure - revert to exact previous state
- updateTasks(previousTasks);
-
- const newMap = new Map(prev);
- newMap.delete(taskId);
-
- const errorMessage = error instanceof Error ? error.message : 'Failed to move task';
- showToast(`Failed to move task: ${errorMessage}`, 'error');
-
- return newMap;
- }
- return prev; // Don't rollback, newer operation is active
- });
-
- } finally {
- // (pendingOperations cleanup removed)
- }
- };
-
- const completeTask = (taskId: string) => {
- console.log(`[TasksTab] Calling completeTask for ${taskId}`);
- moveTask(taskId, 'done');
- };
-
- const deleteTask = async (task: Task) => {
- try {
- await projectService.deleteTask(task.id);
- updateTasks(tasks.filter(t => t.id !== task.id));
- showToast(`Task "${task.title}" deleted`, 'success');
- } catch (error) {
- console.error('Failed to delete task:', error);
- showToast('Failed to delete task', 'error');
- }
- };
-
- // Inline task creation function
- const createTaskInline = async (newTask: Omit) => {
- try {
- // Auto-assign next order number if not provided
- const nextOrder = newTask.task_order || getNextOrderForStatus(newTask.status);
-
- const createData: CreateTaskRequest = {
- project_id: projectId,
- title: newTask.title,
- description: newTask.description,
- status: newTask.status,
- assignee: newTask.assignee?.name || 'User',
- task_order: nextOrder,
- ...(newTask.feature && { feature: newTask.feature }),
- ...(newTask.featureColor && { featureColor: newTask.featureColor })
- };
-
- await projectService.createTask(createData);
-
- // Task created - polling will pick up changes automatically
- console.log('[TasksTab] Task created successfully');
-
- } catch (error) {
- console.error('Failed to create task:', error);
- throw error;
- }
- };
-
- // Inline task update function
- const updateTaskInline = async (taskId: string, updates: Partial) => {
- console.log(`[TasksTab] Inline update for task ${taskId} with updates:`, updates);
- try {
- const updateData: Partial = {
- client_timestamp: Date.now()
- };
-
- if (updates.title !== undefined) updateData.title = updates.title;
- if (updates.description !== undefined) updateData.description = updates.description;
- if (updates.status !== undefined) {
- console.log(`[TasksTab] Setting status for ${taskId}: ${updates.status}`);
- updateData.status = updates.status;
- }
- if (updates.assignee !== undefined) updateData.assignee = updates.assignee.name;
- if (updates.task_order !== undefined) updateData.task_order = updates.task_order;
- if (updates.feature !== undefined) updateData.feature = updates.feature;
- if (updates.featureColor !== undefined) updateData.featureColor = updates.featureColor;
-
- console.log(`[TasksTab] Sending update request for task ${taskId} to projectService:`, updateData);
- await projectService.updateTask(taskId, updateData);
- console.log(`[TasksTab] projectService.updateTask successful for ${taskId}.`);
-
- // Task updated - polling will pick up changes automatically
- console.log(`[TasksTab] Task ${taskId} updated successfully`);
-
- } catch (error) {
- console.error(`[TasksTab] Failed to update task ${taskId} inline:`, error);
- showToast(`Failed to update task: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error');
- throw error;
- }
- };
-
- // Get tasks for priority selection with descriptive labels
- const getTasksForPrioritySelection = (status: Task['status']): Array<{value: number, label: string}> => {
- const tasksInStatus = tasks
- .filter(task => task.status === status && task.id !== editingTask?.id) // Exclude current task if editing
- .sort((a, b) => a.task_order - b.task_order);
-
- const options: Array<{value: number, label: string}> = [];
-
- if (tasksInStatus.length === 0) {
- // No tasks in this status
- options.push({ value: 1, label: "1 - First task in this status" });
- } else {
- // Add option to be first
- options.push({
- value: 1,
- label: `1 - Before "${tasksInStatus[0].title.substring(0, 30)}${tasksInStatus[0].title.length > 30 ? '...' : ''}"`
- });
-
- // Add options between existing tasks
- for (let i = 0; i < tasksInStatus.length - 1; i++) {
- const currentTask = tasksInStatus[i];
- const nextTask = tasksInStatus[i + 1];
- options.push({
- value: i + 2,
- label: `${i + 2} - After "${currentTask.title.substring(0, 20)}${currentTask.title.length > 20 ? '...' : ''}", Before "${nextTask.title.substring(0, 20)}${nextTask.title.length > 20 ? '...' : ''}"`
- });
- }
-
- // Add option to be last
- const lastTask = tasksInStatus[tasksInStatus.length - 1];
- options.push({
- value: tasksInStatus.length + 1,
- label: `${tasksInStatus.length + 1} - After "${lastTask.title.substring(0, 30)}${lastTask.title.length > 30 ? '...' : ''}"`
- });
- }
-
- return options;
- };
-
- // Memoized version of getTasksForPrioritySelection to prevent recalculation on every render
- const memoizedGetTasksForPrioritySelection = useMemo(
- () => getTasksForPrioritySelection,
- [tasks, editingTask?.id]
- );
-
- return (
-
-
- {/* Main content - Table or Board view */}
-
- {viewMode === 'table' ? (
-
- ) : (
-
- )}
-
-
- {/* Fixed View Controls */}
-
-
-
- {/* Add Task Button with Luminous Style */}
-
{
- const defaultOrder = getDefaultTaskOrder(tasks.filter(t => t.status === 'todo'));
- setEditingTask({
- id: '',
- title: '',
- description: '',
- status: 'todo',
- assignee: { name: 'AI IDE Agent', avatar: '' },
- feature: '',
- featureColor: '#3b82f6',
- task_order: defaultOrder
- });
- setIsModalOpen(true);
- }}
- className="relative px-5 py-2.5 flex items-center gap-2 bg-white/80 dark:bg-black/90 border border-gray-200 dark:border-gray-800 rounded-lg shadow-[0_0_20px_rgba(0,0,0,0.1)] dark:shadow-[0_0_20px_rgba(0,0,0,0.5)] backdrop-blur-md pointer-events-auto text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 transition-all duration-300"
- >
-
- Add Task
-
-
-
- {/* View Toggle Controls */}
-
-
setViewMode('table')}
- className={`px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300 ${viewMode === 'table' ? 'text-cyan-600 dark:text-cyan-400' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}`}
- >
-
- Table
- {viewMode === 'table' && }
-
-
setViewMode('board')}
- className={`px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300 ${viewMode === 'board' ? 'text-purple-600 dark:text-purple-400' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300'}`}
- >
-
- Board
- {viewMode === 'board' && }
-
-
-
-
-
- {/* Edit Task Modal */}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/project-tasks/VersionHistoryModal.tsx b/archon-ui-main/src/components/project-tasks/VersionHistoryModal.tsx
deleted file mode 100644
index 8ab2325240..0000000000
--- a/archon-ui-main/src/components/project-tasks/VersionHistoryModal.tsx
+++ /dev/null
@@ -1,661 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { X, Clock, RotateCcw, Eye, Calendar, User, FileText, Diff, GitBranch, Layers, Plus, Minus, AlertTriangle } from 'lucide-react';
-import projectService from '../../services/projectService';
-import { Button } from '../ui/Button';
-import { useToast } from '../../contexts/ToastContext';
-
-interface Version {
- id: string;
- version_number: number;
- change_summary: string;
- change_type: string;
- created_by: string;
- created_at: string;
- content: any;
- document_id?: string;
-}
-
-interface VersionHistoryModalProps {
- isOpen: boolean;
- onClose: () => void;
- projectId: string;
- documentId?: string;
- fieldName?: string;
- onRestore?: () => void;
-}
-
-interface DiffLine {
- type: 'added' | 'removed' | 'unchanged';
- content: string;
- lineNumber?: number;
-}
-
-interface RestoreConfirmModalProps {
- isOpen: boolean;
- versionNumber: number;
- onConfirm: () => void;
- onCancel: () => void;
-}
-
-const RestoreConfirmModal: React.FC = ({
- isOpen,
- versionNumber,
- onConfirm,
- onCancel
-}) => {
- if (!isOpen) return null;
-
- return (
-
-
-
-
-
-
-
-
- Restore Version
-
-
- This will create a new version
-
-
-
-
-
- Are you sure you want to restore to version {versionNumber} ?
- This will create a new version with the restored content.
-
-
-
-
- Cancel
-
- }
- >
- Restore Version
-
-
-
-
-
- );
-};
-
-export const VersionHistoryModal: React.FC = ({
- isOpen,
- onClose,
- projectId,
- documentId,
- fieldName = 'docs',
- onRestore
-}) => {
- const [versions, setVersions] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [previewContent, setPreviewContent] = useState(null);
- const [previewVersion, setPreviewVersion] = useState(null);
- const [restoring, setRestoring] = useState(null);
- const [currentContent, setCurrentContent] = useState(null);
- const [viewMode, setViewMode] = useState<'diff' | 'rendered'>('diff');
- const [showRestoreConfirm, setShowRestoreConfirm] = useState(false);
- const [versionToRestore, setVersionToRestore] = useState(null);
-
- const { showToast } = useToast();
-
- useEffect(() => {
- if (isOpen && projectId) {
- loadVersionHistory();
- loadCurrentContent();
- }
- }, [isOpen, projectId, fieldName, documentId]);
-
- const loadVersionHistory = async () => {
- setLoading(true);
- setError(null);
-
- try {
- const versionData = await projectService.getDocumentVersionHistory(projectId, fieldName);
-
- // Filter versions by document if documentId is provided
- let filteredVersions = versionData || [];
- if (documentId) {
- filteredVersions = versionData.filter((version: Version) => {
- // Check if this version contains changes to the specific document
- if (version.document_id === documentId) {
- return true;
- }
- // Also check if the content contains the document
- if (Array.isArray(version.content)) {
- return version.content.some((doc: any) => doc.id === documentId);
- }
- return false;
- });
- }
-
- setVersions(filteredVersions);
- } catch (error) {
- console.error('Error loading version history:', error);
- setError('Failed to load version history');
- showToast('Failed to load version history', 'error');
- } finally {
- setLoading(false);
- }
- };
-
- const loadCurrentContent = async () => {
- try {
- const currentProject = await projectService.getProject(projectId);
- setCurrentContent((currentProject as any)[fieldName] || []);
- } catch (error) {
- console.error('Error loading current content:', error);
- showToast('Failed to load current content', 'error');
- }
- };
-
- const handlePreview = async (versionNumber: number) => {
- try {
- setPreviewVersion(versionNumber);
- const contentData = await projectService.getVersionContent(projectId, versionNumber, fieldName);
- setPreviewContent(contentData.content);
- } catch (error) {
- console.error('Error loading version content:', error);
- setError('Failed to load version content');
- showToast('Failed to load version content', 'error');
- }
- };
-
- const handleRestoreClick = (versionNumber: number) => {
- setVersionToRestore(versionNumber);
- setShowRestoreConfirm(true);
- };
-
- const handleRestoreConfirm = async () => {
- if (!versionToRestore) return;
-
- setRestoring(versionToRestore);
- setError(null);
- setShowRestoreConfirm(false);
-
- try {
- await projectService.restoreDocumentVersion(projectId, versionToRestore, fieldName);
- await loadVersionHistory();
- await loadCurrentContent();
-
- if (onRestore) {
- onRestore();
- }
-
- showToast(`Successfully restored to version ${versionToRestore}`, 'success');
- } catch (error) {
- console.error('Error restoring version:', error);
- setError('Failed to restore version');
- showToast('Failed to restore version', 'error');
- } finally {
- setRestoring(null);
- setVersionToRestore(null);
- }
- };
-
- const handleRestoreCancel = () => {
- setShowRestoreConfirm(false);
- setVersionToRestore(null);
- };
-
- const formatDate = (dateString: string) => {
- return new Date(dateString).toLocaleString();
- };
-
- const getChangeTypeIcon = (changeType: string) => {
- switch (changeType) {
- case 'create':
- return ;
- case 'update':
- return ;
- case 'delete':
- return ;
- case 'restore':
- return ;
- default:
- return ;
- }
- };
-
- const extractTextContent = (content: any, docId?: string): string => {
- if (!content) return '';
-
- // If content is an array of documents
- if (Array.isArray(content)) {
- // If we have a documentId, filter to just that document
- if (docId) {
- const doc = content.find(d => d.id === docId);
- if (doc) {
- // If it has markdown content, return that
- if (doc.content?.markdown) {
- return doc.content.markdown;
- }
- // Otherwise try to extract text content
- return extractDocumentText(doc);
- }
- return 'Document not found in this version';
- }
- // Otherwise show all documents
- return content.map(doc => {
- if (doc.content?.markdown) {
- return `=== ${doc.title || 'Document'} ===\n${doc.content.markdown}`;
- }
- return `=== ${doc.title || 'Document'} ===\n${extractDocumentText(doc)}`;
- }).join('\n\n');
- }
-
- // If content is an object with markdown
- if (typeof content === 'object' && content.markdown) {
- return content.markdown;
- }
-
- if (typeof content === 'object') {
- return JSON.stringify(content, null, 2);
- }
-
- return String(content);
- };
-
- const extractDocumentText = (doc: any): string => {
- let text = '';
- if (doc.blocks) {
- text = doc.blocks.map((block: any) => {
- if (block.content) {
- return block.content;
- }
- return '';
- }).filter(Boolean).join('\n');
- } else if (doc.content && typeof doc.content === 'string') {
- text = doc.content;
- } else if (doc.content && typeof doc.content === 'object') {
- text = JSON.stringify(doc.content, null, 2);
- }
- return text;
- };
-
- const generateDiff = (oldContent: any, newContent: any): DiffLine[] => {
- const oldText = extractTextContent(oldContent, documentId);
- const newText = extractTextContent(newContent, documentId);
-
- const oldLines = oldText.split('\n');
- const newLines = newText.split('\n');
-
- const diff: DiffLine[] = [];
-
- // Simple line-by-line diff (in a real app you'd use a proper diff library)
- const maxLines = Math.max(oldLines.length, newLines.length);
-
- for (let i = 0; i < maxLines; i++) {
- const oldLine = oldLines[i] || '';
- const newLine = newLines[i] || '';
-
- if (oldLine === newLine) {
- if (oldLine) {
- diff.push({ type: 'unchanged', content: oldLine, lineNumber: i + 1 });
- }
- } else {
- if (oldLine && !newLines.includes(oldLine)) {
- diff.push({ type: 'removed', content: oldLine, lineNumber: i + 1 });
- }
- if (newLine && !oldLines.includes(newLine)) {
- diff.push({ type: 'added', content: newLine, lineNumber: i + 1 });
- }
- }
- }
-
- return diff;
- };
-
- const renderInlineDiff = () => {
- if (!previewContent || !currentContent) {
- return (
-
-
-
Select a version to see changes
-
- );
- }
-
- const diffLines = generateDiff(previewContent, currentContent);
-
- // If filtering by document but no changes found
- if (documentId && diffLines.length === 0) {
- return (
-
-
-
No changes found for this document in the selected version
-
- );
- }
-
- return (
-
-
-
- Comparing Version {previewVersion} → Current
-
-
-
-
- Changes
-
-
-
- {diffLines.map((line, index) => (
-
-
- {line.lineNumber}
-
-
- {line.type === 'added' && }
- {line.type === 'removed' && }
-
-
- {line.content || ' '}
-
-
- ))}
-
-
-
- );
- };
-
- const renderDocumentContent = (content: any) => {
- if (!content) return No content available
;
-
- // Extract the markdown content for the specific document
- const markdownContent = extractTextContent(content, documentId);
-
- if (markdownContent === 'Document not found in this version') {
- return (
-
-
-
Document not found in this version
-
- );
- }
-
- // Render the markdown content
- return (
-
-
- {markdownContent}
-
-
- );
-
- // Old array handling code - keeping for reference but not using
- if (Array.isArray(content) && false) {
- return (
-
- {content.map((doc, index) => (
-
-
-
-
{doc.title || `Document ${index + 1}`}
-
- {doc.blocks && (
-
- {doc.blocks.map((block: any, blockIndex: number) => (
-
- {block.type === 'heading_1' && (
-
{block.content}
- )}
- {block.type === 'heading_2' && (
-
{block.content}
- )}
- {block.type === 'heading_3' && (
-
{block.content}
- )}
- {block.type === 'paragraph' && (
-
{block.content}
- )}
- {block.type === 'bulletListItem' && (
-
- )}
-
- ))}
-
- )}
-
- ))}
-
- );
- }
-
- if (typeof content === 'object') {
- return (
-
-
- {JSON.stringify(content, null, 2)}
-
-
- );
- }
-
- return Unsupported content type
;
- };
-
- if (!isOpen) return null;
-
- return (
- <>
-
-
- {/* Neon top edge */}
-
-
- {/* Header */}
-
-
-
- Version History
- - {fieldName}{documentId ? ' (Document Filtered)' : ''}
-
-
-
-
-
-
- {/* Content */}
-
- {/* Version List */}
-
-
-
-
- Versions
-
-
- {loading && (
-
-
-
Loading versions...
-
- )}
-
- {error && (
-
- )}
-
- {!loading && versions.length === 0 && (
-
- )}
-
-
- {versions.map((version) => (
-
handlePreview(version.version_number)}
- >
-
-
- {getChangeTypeIcon(version.change_type)}
-
- Version {version.version_number}
-
-
-
-
{
- e.stopPropagation();
- handleRestoreClick(version.version_number);
- }}
- disabled={restoring === version.version_number}
- icon={restoring === version.version_number ?
-
:
-
- }
- >
- Restore
-
-
-
-
-
- {version.change_summary}
- {version.document_id && documentId && version.document_id === documentId && (
-
- This document
-
- )}
-
-
-
-
-
- {version.created_by}
-
-
-
- {formatDate(version.created_at)}
-
-
-
- ))}
-
-
-
-
- {/* Preview Panel */}
-
-
-
-
-
- {viewMode === 'diff' ? 'Changes' : 'Content'}
-
-
- {previewVersion !== null && (
-
- setViewMode('diff')}
- icon={ }
- >
- Diff View
-
- setViewMode('rendered')}
- icon={ }
- >
- Rendered
-
-
- )}
-
-
- {previewVersion === null ? (
-
-
-
Select a version to preview
-
- ) : (
-
- {viewMode === 'diff' ? renderInlineDiff() : renderDocumentContent(previewContent)}
-
- )}
-
-
-
-
- {/* Footer */}
-
-
-
-
- {/* Restore Confirmation Modal */}
-
- >
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/PRPViewer.css b/archon-ui-main/src/components/prp/PRPViewer.css
deleted file mode 100644
index f9a5092c86..0000000000
--- a/archon-ui-main/src/components/prp/PRPViewer.css
+++ /dev/null
@@ -1,304 +0,0 @@
-/* PRP Viewer Styles - Beautiful Archon Theme */
-
-.prp-viewer {
- animation: fadeIn 0.5s ease-out;
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* Smooth collapse animations */
-.prp-viewer .collapsible-content {
- transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
- opacity 0.3s ease-out;
-}
-
-/* Hover effects for cards */
-.prp-viewer .persona-card,
-.prp-viewer .metric-item,
-.prp-viewer .flow-diagram {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-/* Glow effects for icons */
-.prp-viewer .icon-glow {
- filter: drop-shadow(0 0 8px currentColor);
-}
-
-/* Gradient text animations */
-@keyframes gradientShift {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
-}
-
-.prp-viewer .gradient-text {
- background-size: 200% 200%;
- animation: gradientShift 3s ease infinite;
-}
-
-/* Section reveal animations */
-.prp-viewer .section-content {
- animation: slideIn 0.4s ease-out;
-}
-
-@keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(-20px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-/* Pulse animation for important metrics */
-@keyframes metricPulse {
- 0%, 100% {
- transform: scale(1);
- opacity: 1;
- }
- 50% {
- transform: scale(1.05);
- opacity: 0.8;
- }
-}
-
-.prp-viewer .metric-highlight {
- animation: metricPulse 2s ease-in-out infinite;
-}
-
-/* Flow diagram connections */
-.prp-viewer .flow-connection {
- position: relative;
-}
-
-.prp-viewer .flow-connection::before {
- content: '';
- position: absolute;
- left: -12px;
- top: 50%;
- width: 8px;
- height: 8px;
- background: linear-gradient(135deg, #3b82f6, #a855f7);
- border-radius: 50%;
- transform: translateY(-50%);
- box-shadow: 0 0 12px rgba(59, 130, 246, 0.6);
-}
-
-/* Interactive hover states */
-.prp-viewer .interactive-section:hover {
- transform: translateY(-2px);
- box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1),
- 0 10px 10px -5px rgba(0, 0, 0, 0.04);
-}
-
-/* Dark mode enhancements */
-.dark .prp-viewer .interactive-section:hover {
- box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5),
- 0 10px 10px -5px rgba(0, 0, 0, 0.2),
- 0 0 20px rgba(59, 130, 246, 0.3);
-}
-
-/* Loading skeleton animation */
-@keyframes shimmer {
- 0% {
- background-position: -200% 0;
- }
- 100% {
- background-position: 200% 0;
- }
-}
-
-.prp-viewer .skeleton {
- background: linear-gradient(
- 90deg,
- rgba(255, 255, 255, 0) 0%,
- rgba(255, 255, 255, 0.2) 50%,
- rgba(255, 255, 255, 0) 100%
- );
- background-size: 200% 100%;
- animation: shimmer 1.5s infinite;
-}
-
-/* Collapsible chevron animation */
-.prp-viewer .chevron-animate {
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-/* Card entrance animations */
-.prp-viewer .card-entrance {
- animation: cardSlideUp 0.5s ease-out;
- animation-fill-mode: both;
-}
-
-@keyframes cardSlideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* Stagger animation for lists */
-.prp-viewer .stagger-item {
- animation: fadeInUp 0.4s ease-out;
- animation-fill-mode: both;
-}
-
-.prp-viewer .stagger-item:nth-child(1) { animation-delay: 0.1s; }
-.prp-viewer .stagger-item:nth-child(2) { animation-delay: 0.2s; }
-.prp-viewer .stagger-item:nth-child(3) { animation-delay: 0.3s; }
-.prp-viewer .stagger-item:nth-child(4) { animation-delay: 0.4s; }
-.prp-viewer .stagger-item:nth-child(5) { animation-delay: 0.5s; }
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* Floating animation for icons */
-@keyframes float {
- 0%, 100% {
- transform: translateY(0);
- }
- 50% {
- transform: translateY(-5px);
- }
-}
-
-.prp-viewer .float-icon {
- animation: float 3s ease-in-out infinite;
-}
-
-/* Glow border effect */
-.prp-viewer .glow-border {
- position: relative;
- overflow: hidden;
-}
-
-.prp-viewer .glow-border::before {
- content: '';
- position: absolute;
- top: -2px;
- left: -2px;
- right: -2px;
- bottom: -2px;
- background: linear-gradient(45deg, #3b82f6, #a855f7, #ec4899, #3b82f6);
- border-radius: inherit;
- opacity: 0;
- transition: opacity 0.3s ease;
- z-index: -1;
- background-size: 400% 400%;
- animation: gradientRotate 3s ease infinite;
-}
-
-.prp-viewer .glow-border:hover::before {
- opacity: 1;
-}
-
-@keyframes gradientRotate {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
-}
-
-/* Success metric animations */
-.prp-viewer .metric-success {
- position: relative;
-}
-
-.prp-viewer .metric-success::after {
- content: '✓';
- position: absolute;
- right: -20px;
- top: 50%;
- transform: translateY(-50%);
- color: #10b981;
- font-weight: bold;
- opacity: 0;
- transition: all 0.3s ease;
-}
-
-.prp-viewer .metric-success:hover::after {
- opacity: 1;
- right: 10px;
-}
-
-/* Smooth scrolling for sections */
-.prp-viewer {
- scroll-behavior: smooth;
-}
-
-/* Progress indicator for implementation phases */
-.prp-viewer .phase-progress {
- position: relative;
- padding-left: 30px;
-}
-
-.prp-viewer .phase-progress::before {
- content: '';
- position: absolute;
- left: 10px;
- top: 0;
- bottom: 0;
- width: 2px;
- background: linear-gradient(to bottom, #3b82f6, #a855f7);
-}
-
-.prp-viewer .phase-progress .phase-dot {
- position: absolute;
- left: 6px;
- top: 20px;
- width: 10px;
- height: 10px;
- background: white;
- border: 2px solid #3b82f6;
- border-radius: 50%;
- z-index: 1;
-}
-
-/* Responsive adjustments */
-@media (max-width: 768px) {
- .prp-viewer .grid {
- grid-template-columns: 1fr;
- }
-
- .prp-viewer .text-3xl {
- font-size: 1.5rem;
- }
-
- .prp-viewer .p-6 {
- padding: 1rem;
- }
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/PRPViewer.tsx b/archon-ui-main/src/components/prp/PRPViewer.tsx
deleted file mode 100644
index 84da80f66c..0000000000
--- a/archon-ui-main/src/components/prp/PRPViewer.tsx
+++ /dev/null
@@ -1,279 +0,0 @@
-import React from 'react';
-import { PRPContent } from './types/prp.types';
-import { MetadataSection } from './sections/MetadataSection';
-import { SectionRenderer } from './renderers/SectionRenderer';
-import { normalizePRPDocument } from './utils/normalizer';
-import { processContentForPRP, isMarkdownContent, isDocumentWithMetadata } from './utils/markdownParser';
-import { MarkdownDocumentRenderer } from './components/MarkdownDocumentRenderer';
-import './PRPViewer.css';
-
-interface PRPViewerProps {
- content: PRPContent;
- isDarkMode?: boolean;
- sectionOverrides?: Record>;
-}
-
-/**
- * Process content to handle [Image #N] placeholders
- */
-const processContent = (content: any): any => {
- if (typeof content === 'string') {
- // Replace [Image #N] with proper markdown image syntax
- return content.replace(/\[Image #(\d+)\]/g, (match, num) => {
- return ``;
- });
- }
-
- if (Array.isArray(content)) {
- return content.map(item => processContent(item));
- }
-
- if (typeof content === 'object' && content !== null) {
- const processed: any = {};
- for (const [key, value] of Object.entries(content)) {
- processed[key] = processContent(value);
- }
- return processed;
- }
-
- return content;
-};
-
-/**
- * Flexible PRP Viewer that dynamically renders sections based on content structure
- */
-export const PRPViewer: React.FC = ({
- content,
- isDarkMode = false,
- sectionOverrides = {}
-}) => {
- try {
- if (!content) {
- return No PRP content available
;
- }
-
- console.log('PRPViewer: Received content:', {
- type: typeof content,
- isString: typeof content === 'string',
- isObject: typeof content === 'object',
- hasMetadata: typeof content === 'object' && content !== null ? isDocumentWithMetadata(content) : false,
- isMarkdown: typeof content === 'string' ? isMarkdownContent(content) : false,
- keys: typeof content === 'object' && content !== null ? Object.keys(content) : [],
- contentPreview: typeof content === 'string' ? content.substring(0, 200) + '...' : 'Not a string'
- });
-
- // Route to appropriate renderer based on content type
-
- // 1. Check if it's a document with metadata + markdown content
- if (isDocumentWithMetadata(content)) {
- console.log('PRPViewer: Detected document with metadata, using MarkdownDocumentRenderer');
- return (
-
- );
- }
-
- // 2. Check if it's a pure markdown string
- if (typeof content === 'string' && isMarkdownContent(content)) {
- console.log('PRPViewer: Detected pure markdown content, using MarkdownDocumentRenderer');
- return (
-
- );
- }
-
- // 3. Check if it's an object that might contain markdown content in any field
- if (typeof content === 'object' && content !== null) {
- // Check for markdown field first (common in PRP documents)
- if (typeof content.markdown === 'string') {
- console.log('PRPViewer: Found markdown field, using MarkdownDocumentRenderer');
- return (
-
- );
- }
-
- // Look for markdown content in any field
- for (const [key, value] of Object.entries(content)) {
- if (typeof value === 'string' && isMarkdownContent(value)) {
- console.log(`PRPViewer: Found markdown content in field '${key}', using MarkdownDocumentRenderer`);
- // Create a proper document structure
- const documentContent = {
- title: content.title || 'Document',
- content: value,
- ...content // Include all other fields as metadata
- };
- return (
-
- );
- }
- }
- }
-
- // 4. For any other content that might contain documents, try MarkdownDocumentRenderer first
- console.log('PRPViewer: Checking if content should use MarkdownDocumentRenderer anyway');
-
- // If it's an object with any text content, try MarkdownDocumentRenderer
- if (typeof content === 'object' && content !== null) {
- const hasAnyTextContent = Object.values(content).some(value =>
- typeof value === 'string' && value.length > 50
- );
-
- if (hasAnyTextContent) {
- console.log('PRPViewer: Object has substantial text content, trying MarkdownDocumentRenderer');
- return (
-
- );
- }
- }
-
- // 5. Final fallback to original PRPViewer logic for purely structured JSON content
- console.log('PRPViewer: Using standard JSON structure renderer as final fallback');
-
- // First, check if content is raw markdown and process it
- let processedForPRP = content;
-
- // Handle the case where content is a raw markdown string (non-markdown strings)
- if (typeof content === 'string') {
- // For non-markdown strings, wrap in a simple structure
- processedForPRP = {
- title: 'Document Content',
- content: content,
- document_type: 'text'
- };
- } else if (typeof content === 'object' && content !== null) {
- // For objects, process normally
- processedForPRP = processContentForPRP(content);
- }
-
- // Ensure we have an object to work with
- if (!processedForPRP || typeof processedForPRP !== 'object') {
- return Unable to process PRP content
;
- }
-
- // Normalize the content
- const normalizedContent = normalizePRPDocument(processedForPRP);
-
- // Process content to handle [Image #N] placeholders
- const processedContent = processContent(normalizedContent);
-
- // Extract sections (skip metadata fields)
- const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'id', '_id', 'project_id', 'created_at', 'updated_at'];
- const sections = Object.entries(processedContent).filter(([key]) => !metadataFields.includes(key));
-
- // Debug: Log sections being rendered
- console.log('PRP Sections found:', sections.map(([key]) => key));
-
- // Priority-based sorting for common PRP sections
- const getSectionPriority = (key: string): number => {
- const normalizedKey = key.toLowerCase();
-
- // Define priority order (lower number = higher priority)
- if (normalizedKey.includes('goal') || normalizedKey.includes('objective')) return 1;
- if (normalizedKey.includes('why') || normalizedKey.includes('rationale')) return 2;
- if (normalizedKey.includes('what') || normalizedKey === 'description') return 3;
- if (normalizedKey.includes('context') || normalizedKey.includes('background')) return 4;
- if (normalizedKey.includes('persona') || normalizedKey.includes('user') || normalizedKey.includes('stakeholder')) return 5;
- if (normalizedKey.includes('flow') || normalizedKey.includes('journey') || normalizedKey.includes('workflow')) return 6;
- if (normalizedKey.includes('requirement') && !normalizedKey.includes('technical')) return 7;
- if (normalizedKey.includes('metric') || normalizedKey.includes('success') || normalizedKey.includes('kpi')) return 8;
- if (normalizedKey.includes('timeline') || normalizedKey.includes('roadmap') || normalizedKey.includes('milestone')) return 9;
- if (normalizedKey.includes('plan') || normalizedKey.includes('implementation')) return 10;
- if (normalizedKey.includes('technical') || normalizedKey.includes('architecture') || normalizedKey.includes('tech')) return 11;
- if (normalizedKey.includes('validation') || normalizedKey.includes('testing') || normalizedKey.includes('quality')) return 12;
- if (normalizedKey.includes('risk') || normalizedKey.includes('mitigation')) return 13;
-
- // Default priority for unknown sections
- return 50;
- };
-
- // Sort sections by priority
- const sortedSections = sections.sort(([a], [b]) => {
- return getSectionPriority(a) - getSectionPriority(b);
- });
-
- return (
-
- {/* Metadata Header */}
-
-
- {/* Dynamic Sections */}
- {sortedSections.map(([sectionKey, sectionData], index) => (
-
-
-
- ))}
-
- {sections.length === 0 && (
-
-
No additional sections found in this PRP document.
-
- )}
-
- );
- } catch (error) {
- console.error('PRPViewer: Error rendering content:', error);
-
- // Provide a meaningful error display instead of black screen
- return (
-
-
Error Rendering PRP
-
- There was an error rendering this PRP document. The content may be in an unexpected format.
-
-
- {/* Show error details for debugging */}
-
-
- Show error details
-
-
-
- {error instanceof Error ? error.message : String(error)}
-
- {error instanceof Error && error.stack && (
-
- {error.stack}
-
- )}
-
-
-
- {/* Show raw content for debugging */}
-
-
- Show raw content
-
-
- {typeof content === 'string'
- ? content
- : JSON.stringify(content, null, 2)}
-
-
-
- );
- }
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/components/CollapsibleSectionRenderer.tsx b/archon-ui-main/src/components/prp/components/CollapsibleSectionRenderer.tsx
deleted file mode 100644
index 26ee82d58b..0000000000
--- a/archon-ui-main/src/components/prp/components/CollapsibleSectionRenderer.tsx
+++ /dev/null
@@ -1,411 +0,0 @@
-import React, { useState, useEffect, ReactNode } from 'react';
-import {
- ChevronDown,
- Brain, Users, Workflow, BarChart3, Clock, Shield,
- Code, Layers, FileText, List, Hash, Box, Type, ToggleLeft,
- CheckCircle, AlertCircle, Info, Lightbulb
-} from 'lucide-react';
-import { SectionProps } from '../types/prp.types';
-import { SimpleMarkdown } from './SimpleMarkdown';
-import { formatValue } from '../utils/formatters';
-
-interface CollapsibleSectionRendererProps extends SectionProps {
- children?: ReactNode;
- headerContent?: ReactNode;
- sectionKey?: string;
- contentType?: 'markdown' | 'code' | 'json' | 'list' | 'object' | 'auto';
- animationDuration?: number;
- showPreview?: boolean;
- previewLines?: number;
-}
-
-/**
- * Enhanced CollapsibleSectionRenderer with beautiful animations and content-aware styling
- * Features:
- * - Section-specific icons and colors
- * - Smooth expand/collapse animations with dynamic height
- * - Content type detection and appropriate formatting
- * - Code block syntax highlighting support
- * - Nested structure handling
- * - Preview mode for collapsed content
- */
-export const CollapsibleSectionRenderer: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'gray',
- defaultOpen = true,
- isDarkMode = false,
- isCollapsible = true,
- isOpen: controlledIsOpen,
- onToggle,
- children,
- headerContent,
- sectionKey = '',
- contentType = 'auto',
- animationDuration = 300,
- showPreview = true,
- previewLines = 2
-}) => {
- // State management for collapsible behavior
- const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
- const [contentHeight, setContentHeight] = useState('auto');
- const [isAnimating, setIsAnimating] = useState(false);
-
- const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
-
- // Content ref for measuring height
- const contentRef = React.useRef(null);
-
- useEffect(() => {
- if (controlledIsOpen === undefined) {
- setInternalIsOpen(defaultOpen);
- }
- }, [defaultOpen, controlledIsOpen]);
-
- // Measure content height for smooth animations
- useEffect(() => {
- if (contentRef.current && isCollapsible) {
- const height = contentRef.current.scrollHeight;
- setContentHeight(isOpen ? height : 0);
- }
- }, [isOpen, data, children]);
-
- const handleToggle = () => {
- if (!isCollapsible) return;
-
- setIsAnimating(true);
-
- if (controlledIsOpen === undefined) {
- setInternalIsOpen(!internalIsOpen);
- }
- onToggle?.();
-
- // Reset animation state after duration
- setTimeout(() => setIsAnimating(false), animationDuration);
- };
-
- // Auto-detect section type and get appropriate icon
- const getSectionIcon = (): ReactNode => {
- if (icon) return icon;
-
- const normalizedKey = sectionKey.toLowerCase();
- const normalizedTitle = title.toLowerCase();
-
- // Check both section key and title for better detection
- const checkKeywords = (keywords: string[]) =>
- keywords.some(keyword =>
- normalizedKey.includes(keyword) || normalizedTitle.includes(keyword)
- );
-
- if (checkKeywords(['context', 'overview', 'background']))
- return ;
- if (checkKeywords(['persona', 'user', 'actor', 'stakeholder']))
- return ;
- if (checkKeywords(['flow', 'journey', 'workflow', 'process']))
- return ;
- if (checkKeywords(['metric', 'success', 'kpi', 'measurement']))
- return ;
- if (checkKeywords(['plan', 'implementation', 'roadmap', 'timeline']))
- return ;
- if (checkKeywords(['validation', 'gate', 'criteria', 'acceptance']))
- return ;
- if (checkKeywords(['technical', 'tech', 'architecture', 'system']))
- return ;
- if (checkKeywords(['architecture', 'structure', 'design']))
- return ;
- if (checkKeywords(['feature', 'functionality', 'capability']))
- return ;
- if (checkKeywords(['requirement', 'spec', 'specification']))
- return ;
- if (checkKeywords(['risk', 'issue', 'concern', 'challenge']))
- return ;
- if (checkKeywords(['info', 'note', 'detail']))
- return ;
-
- // Fallback based on data type
- if (typeof data === 'string') return ;
- if (typeof data === 'number') return ;
- if (typeof data === 'boolean') return ;
- if (Array.isArray(data)) return
;
- if (typeof data === 'object' && data !== null) return ;
-
- return ;
- };
-
- // Get section-specific color scheme
- const getColorScheme = () => {
- const normalizedKey = sectionKey.toLowerCase();
- const normalizedTitle = title.toLowerCase();
-
- const checkKeywords = (keywords: string[]) =>
- keywords.some(keyword =>
- normalizedKey.includes(keyword) || normalizedTitle.includes(keyword)
- );
-
- if (checkKeywords(['context', 'overview'])) return 'blue';
- if (checkKeywords(['persona', 'user'])) return 'purple';
- if (checkKeywords(['flow', 'journey'])) return 'orange';
- if (checkKeywords(['metric', 'success'])) return 'green';
- if (checkKeywords(['plan', 'implementation'])) return 'cyan';
- if (checkKeywords(['validation', 'gate'])) return 'emerald';
- if (checkKeywords(['technical', 'architecture'])) return 'indigo';
- if (checkKeywords(['feature'])) return 'yellow';
- if (checkKeywords(['risk', 'issue'])) return 'red';
-
- return accentColor;
- };
-
- // Auto-detect content type if not specified
- const getContentType = () => {
- if (contentType !== 'auto') return contentType;
-
- if (typeof data === 'string') {
- // Check for code patterns
- if (/^```[\s\S]*```$/m.test(data) ||
- /^\s*(function|class|const|let|var|import|export)\s/m.test(data) ||
- /^\s*[{[][\s\S]*[}\]]$/m.test(data)) {
- return 'code';
- }
-
- // Check for markdown patterns
- if (/^#{1,6}\s+.+$|^[-*+]\s+.+$|^\d+\.\s+.+$|```|^\>.+$|\*\*.+\*\*|\*.+\*|`[^`]+`/m.test(data)) {
- return 'markdown';
- }
- }
-
- if (Array.isArray(data)) return 'list';
- if (typeof data === 'object' && data !== null) {
- try {
- JSON.stringify(data);
- return 'json';
- } catch {
- return 'object';
- }
- }
-
- return 'auto';
- };
-
- // Render content based on type
- const renderContent = (): ReactNode => {
- if (children) return children;
-
- const detectedType = getContentType();
-
- switch (detectedType) {
- case 'markdown':
- return ;
-
- case 'code':
- return (
-
- );
-
- case 'json':
- return (
-
-
- {JSON.stringify(data, null, 2)}
-
-
- );
-
- case 'list':
- if (!Array.isArray(data)) return Invalid list data ;
- return (
-
- {data.map((item, index) => (
-
- •
- {formatValue(item)}
-
- ))}
-
- );
-
- default:
- return {formatValue(data)} ;
- }
- };
-
- // Generate preview content when collapsed
- const renderPreview = (): ReactNode => {
- if (!showPreview || isOpen || !data) return null;
-
- const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
- const lines = dataStr.split('\n').slice(0, previewLines);
- const preview = lines.join('\n');
- const hasMore = dataStr.split('\n').length > previewLines;
-
- return (
-
-
- {preview}
- {hasMore && ... }
-
-
- );
- };
-
- const colorScheme = getColorScheme();
- const sectionIcon = getSectionIcon();
-
- // Color mapping for backgrounds and borders
- const getColorClasses = () => {
- const colorMap = {
- blue: {
- bg: 'bg-blue-50/50 dark:bg-blue-950/20',
- border: 'border-blue-200 dark:border-blue-800',
- iconBg: 'bg-blue-100 dark:bg-blue-900',
- iconText: 'text-blue-600 dark:text-blue-400',
- accent: 'border-l-blue-500'
- },
- purple: {
- bg: 'bg-purple-50/50 dark:bg-purple-950/20',
- border: 'border-purple-200 dark:border-purple-800',
- iconBg: 'bg-purple-100 dark:bg-purple-900',
- iconText: 'text-purple-600 dark:text-purple-400',
- accent: 'border-l-purple-500'
- },
- green: {
- bg: 'bg-green-50/50 dark:bg-green-950/20',
- border: 'border-green-200 dark:border-green-800',
- iconBg: 'bg-green-100 dark:bg-green-900',
- iconText: 'text-green-600 dark:text-green-400',
- accent: 'border-l-green-500'
- },
- orange: {
- bg: 'bg-orange-50/50 dark:bg-orange-950/20',
- border: 'border-orange-200 dark:border-orange-800',
- iconBg: 'bg-orange-100 dark:bg-orange-900',
- iconText: 'text-orange-600 dark:text-orange-400',
- accent: 'border-l-orange-500'
- },
- cyan: {
- bg: 'bg-cyan-50/50 dark:bg-cyan-950/20',
- border: 'border-cyan-200 dark:border-cyan-800',
- iconBg: 'bg-cyan-100 dark:bg-cyan-900',
- iconText: 'text-cyan-600 dark:text-cyan-400',
- accent: 'border-l-cyan-500'
- },
- indigo: {
- bg: 'bg-indigo-50/50 dark:bg-indigo-950/20',
- border: 'border-indigo-200 dark:border-indigo-800',
- iconBg: 'bg-indigo-100 dark:bg-indigo-900',
- iconText: 'text-indigo-600 dark:text-indigo-400',
- accent: 'border-l-indigo-500'
- },
- emerald: {
- bg: 'bg-emerald-50/50 dark:bg-emerald-950/20',
- border: 'border-emerald-200 dark:border-emerald-800',
- iconBg: 'bg-emerald-100 dark:bg-emerald-900',
- iconText: 'text-emerald-600 dark:text-emerald-400',
- accent: 'border-l-emerald-500'
- },
- yellow: {
- bg: 'bg-yellow-50/50 dark:bg-yellow-950/20',
- border: 'border-yellow-200 dark:border-yellow-800',
- iconBg: 'bg-yellow-100 dark:bg-yellow-900',
- iconText: 'text-yellow-600 dark:text-yellow-400',
- accent: 'border-l-yellow-500'
- },
- red: {
- bg: 'bg-red-50/50 dark:bg-red-950/20',
- border: 'border-red-200 dark:border-red-800',
- iconBg: 'bg-red-100 dark:bg-red-900',
- iconText: 'text-red-600 dark:text-red-400',
- accent: 'border-l-red-500'
- },
- gray: {
- bg: 'bg-gray-50/50 dark:bg-gray-950/20',
- border: 'border-gray-200 dark:border-gray-800',
- iconBg: 'bg-gray-100 dark:bg-gray-900',
- iconText: 'text-gray-600 dark:text-gray-400',
- accent: 'border-l-gray-500'
- }
- };
- return colorMap[colorScheme as keyof typeof colorMap] || colorMap.gray;
- };
-
- const colors = getColorClasses();
-
- if (!isCollapsible) {
- return (
-
-
-
-
- {sectionIcon}
-
-
- {title}
-
- {headerContent}
-
-
- {renderContent()}
-
-
-
- );
- }
-
- return (
-
- {/* Header */}
-
-
-
- {sectionIcon}
-
-
- {title}
-
- {headerContent}
-
-
-
-
- {renderPreview()}
-
-
- {/* Content with smooth height animation */}
-
-
- {renderContent()}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/components/CollapsibleSectionWrapper.tsx b/archon-ui-main/src/components/prp/components/CollapsibleSectionWrapper.tsx
deleted file mode 100644
index 052096cbc5..0000000000
--- a/archon-ui-main/src/components/prp/components/CollapsibleSectionWrapper.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React, { useState, useEffect, ReactNode } from 'react';
-import { ChevronDown } from 'lucide-react';
-
-interface CollapsibleSectionWrapperProps {
- children: ReactNode;
- header: ReactNode;
- isCollapsible?: boolean;
- defaultOpen?: boolean;
- isOpen?: boolean;
- onToggle?: () => void;
-}
-
-/**
- * A wrapper component that makes any section collapsible by clicking on its header
- */
-export const CollapsibleSectionWrapper: React.FC = ({
- children,
- header,
- isCollapsible = true,
- defaultOpen = true,
- isOpen: controlledIsOpen,
- onToggle
-}) => {
- // Use controlled state if provided, otherwise manage internally
- const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
- const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
-
- useEffect(() => {
- if (controlledIsOpen === undefined) {
- setInternalIsOpen(defaultOpen);
- }
- }, [defaultOpen, controlledIsOpen]);
-
- const handleToggle = () => {
- if (controlledIsOpen === undefined) {
- setInternalIsOpen(!internalIsOpen);
- }
- onToggle?.();
- };
-
- if (!isCollapsible) {
- return (
- <>
- {header}
- {children}
- >
- );
- }
-
- return (
- <>
-
-
-
- >
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/components/MarkdownDocumentRenderer.tsx b/archon-ui-main/src/components/prp/components/MarkdownDocumentRenderer.tsx
deleted file mode 100644
index d390ecc669..0000000000
--- a/archon-ui-main/src/components/prp/components/MarkdownDocumentRenderer.tsx
+++ /dev/null
@@ -1,323 +0,0 @@
-import React from 'react';
-import { ParsedMarkdownDocument, parseMarkdownToDocument, isDocumentWithMetadata, isMarkdownContent } from '../utils/markdownParser';
-import { MetadataSection } from '../sections/MetadataSection';
-import { MarkdownSectionRenderer } from './MarkdownSectionRenderer';
-
-interface MarkdownDocumentRendererProps {
- content: any;
- isDarkMode?: boolean;
- sectionOverrides?: Record>;
-}
-
-/**
- * Renders markdown documents with metadata header and flowing content sections
- * Handles both pure markdown strings and documents with metadata + content structure
- */
-/**
- * Processes JSON content and converts it to markdown format
- * Handles nested objects, arrays, and various data types
- */
-function processContentToMarkdown(content: any): string {
- if (typeof content === 'string') {
- return content;
- }
-
- if (typeof content !== 'object' || content === null) {
- return String(content);
- }
-
- const markdownSections: string[] = [];
-
- // Extract metadata fields first (don't include in content conversion)
- const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
-
- for (const [key, value] of Object.entries(content)) {
- // Skip metadata fields as they're handled separately
- if (metadataFields.includes(key)) {
- continue;
- }
-
- // Skip null or undefined values
- if (value === null || value === undefined) {
- continue;
- }
-
- const sectionTitle = formatSectionTitle(key);
- const sectionContent = formatSectionContent(value);
-
- if (sectionContent.trim()) {
- markdownSections.push(`## ${sectionTitle}\n\n${sectionContent}`);
- }
- }
-
- return markdownSections.join('\n\n');
-}
-
-/**
- * Formats a section title from a JSON key
- */
-function formatSectionTitle(key: string): string {
- return key
- .replace(/([A-Z])/g, ' $1') // Add space before capital letters
- .replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
- .split(' ')
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
- .join(' ')
- .trim();
-}
-
-/**
- * Formats section content based on its type
- */
-function formatSectionContent(value: any): string {
- if (typeof value === 'string') {
- return value;
- }
-
- if (typeof value === 'number' || typeof value === 'boolean') {
- return String(value);
- }
-
- if (Array.isArray(value)) {
- return formatArrayContent(value);
- }
-
- if (typeof value === 'object' && value !== null) {
- return formatObjectContent(value);
- }
-
- return String(value);
-}
-
-/**
- * Formats array content as markdown list or nested structure
- */
-function formatArrayContent(array: any[]): string {
- if (array.length === 0) {
- return '_No items_';
- }
-
- // Check if all items are simple values (strings, numbers, booleans)
- const allSimple = array.every(item =>
- typeof item === 'string' ||
- typeof item === 'number' ||
- typeof item === 'boolean'
- );
-
- if (allSimple) {
- return array.map(item => `- ${String(item)}`).join('\n');
- }
-
- // Handle complex objects in array
- return array.map((item, index) => {
- if (typeof item === 'object' && item !== null) {
- const title = item.title || item.name || `Item ${index + 1}`;
- const content = formatObjectContent(item, true);
- return `### ${title}\n\n${content}`;
- }
- return `- ${String(item)}`;
- }).join('\n\n');
-}
-
-/**
- * Formats object content as key-value pairs or nested structure
- */
-function formatObjectContent(obj: Record, isNested: boolean = false): string {
- const entries = Object.entries(obj);
-
- if (entries.length === 0) {
- return '_Empty_';
- }
-
- const formatted = entries.map(([key, value]) => {
- if (value === null || value === undefined) {
- return null;
- }
-
- const label = formatSectionTitle(key);
-
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
- return `**${label}:** ${String(value)}`;
- }
-
- if (Array.isArray(value)) {
- const arrayContent = formatArrayContent(value);
- return `**${label}:**\n${arrayContent}`;
- }
-
- if (typeof value === 'object') {
- const nestedContent = formatObjectContent(value, true);
- return `**${label}:**\n${nestedContent}`;
- }
-
- return `**${label}:** ${String(value)}`;
- }).filter(Boolean);
-
- return formatted.join('\n\n');
-}
-export const MarkdownDocumentRenderer: React.FC = ({
- content,
- isDarkMode = false,
- sectionOverrides = {}
-}) => {
- try {
- let parsedDocument: ParsedMarkdownDocument;
- let documentMetadata: any = {};
-
- console.log('MarkdownDocumentRenderer: Processing content:', {
- type: typeof content,
- keys: typeof content === 'object' && content !== null ? Object.keys(content) : [],
- isDocWithMetadata: typeof content === 'object' && content !== null ? isDocumentWithMetadata(content) : false
- });
-
- // Handle different content structures
- if (typeof content === 'string') {
- console.log('MarkdownDocumentRenderer: Processing pure markdown string');
- // Pure markdown string
- parsedDocument = parseMarkdownToDocument(content);
- // Create synthetic metadata for display
- documentMetadata = {
- title: parsedDocument.title || 'Document',
- document_type: 'markdown'
- };
- } else if (typeof content === 'object' && content !== null) {
- console.log('MarkdownDocumentRenderer: Processing object content');
-
- // Extract all potential metadata fields first
- const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
- metadataFields.forEach(field => {
- if (content[field]) {
- documentMetadata[field] = content[field];
- }
- });
-
- // Find the markdown content in any field
- let markdownContent = '';
-
- // First check common markdown field names
- if (typeof content.markdown === 'string') {
- markdownContent = content.markdown;
- console.log('MarkdownDocumentRenderer: Found markdown in "markdown" field');
- } else if (typeof content.content === 'string' && isMarkdownContent(content.content)) {
- markdownContent = content.content;
- console.log('MarkdownDocumentRenderer: Found markdown in "content" field');
- } else {
- // Look for markdown content in any field
- for (const [key, value] of Object.entries(content)) {
- if (typeof value === 'string' && isMarkdownContent(value)) {
- markdownContent = value;
- console.log(`MarkdownDocumentRenderer: Found markdown in field '${key}'`);
- break;
- }
- }
- }
-
- // If no existing markdown found, try to convert JSON structure to markdown
- if (!markdownContent) {
- console.log('MarkdownDocumentRenderer: No markdown found, converting JSON to markdown');
- markdownContent = processContentToMarkdown(content);
- }
-
- if (markdownContent) {
- console.log('MarkdownDocumentRenderer: Parsing markdown content:', {
- contentLength: markdownContent.length,
- contentPreview: markdownContent.substring(0, 100) + '...'
- });
- parsedDocument = parseMarkdownToDocument(markdownContent);
- console.log('MarkdownDocumentRenderer: Parsed document:', {
- sectionsCount: parsedDocument.sections.length,
- sections: parsedDocument.sections.map(s => ({ title: s.title, type: s.type }))
- });
- } else {
- // No markdown content found, create empty document
- console.log('MarkdownDocumentRenderer: No markdown content found in document');
- parsedDocument = { sections: [], metadata: {}, hasMetadata: false };
- }
-
- // Use document title from metadata if available
- if (content.title && !parsedDocument.title) {
- parsedDocument.title = content.title;
- }
- } else {
- console.log('MarkdownDocumentRenderer: Unexpected content structure');
- // Fallback for unexpected content structure
- return (
-
-
Unable to parse document content
-
- );
- }
-
- // ALWAYS show metadata - force hasMetadata to true
- parsedDocument.hasMetadata = true;
-
- // Combine parsed metadata with document metadata and add defaults
- const finalMetadata = {
- // Default values for better display
- document_type: 'prp',
- version: '1.0',
- status: 'draft',
- ...parsedDocument.metadata,
- ...documentMetadata,
- title: parsedDocument.title || documentMetadata.title || 'Untitled Document'
- };
-
- console.log('MarkdownDocumentRenderer: Final render data:', {
- hasMetadata: parsedDocument.hasMetadata,
- finalMetadata,
- sectionsCount: parsedDocument.sections.length,
- sections: parsedDocument.sections.map(s => ({ title: s.title, type: s.type, templateType: s.templateType }))
- });
-
- return (
-
- {/* ALWAYS show metadata header */}
-
-
- {/* Document Sections */}
-
- {parsedDocument.sections.map((section, index) => (
-
- ))}
-
-
- {/* Empty state */}
- {parsedDocument.sections.length === 0 && (
-
-
No content sections found in this document.
-
- )}
-
- );
- } catch (error) {
- console.error('MarkdownDocumentRenderer: Error rendering content:', error);
-
- // Provide a meaningful error display instead of black screen
- return (
-
-
Error Rendering Document
-
- There was an error rendering this document. The content may be in an unexpected format.
-
-
- {/* Show raw content for debugging */}
-
-
- Show raw content
-
-
- {typeof content === 'string'
- ? content
- : JSON.stringify(content, null, 2)}
-
-
-
- );
- }
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/components/MarkdownSectionRenderer.tsx b/archon-ui-main/src/components/prp/components/MarkdownSectionRenderer.tsx
deleted file mode 100644
index 35f541a41c..0000000000
--- a/archon-ui-main/src/components/prp/components/MarkdownSectionRenderer.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from 'react';
-import { ParsedSection } from '../utils/markdownParser';
-import { SectionRenderer } from '../renderers/SectionRenderer';
-import { SimpleMarkdown } from './SimpleMarkdown';
-import { detectSectionType } from '../utils/sectionDetector';
-
-interface MarkdownSectionRendererProps {
- section: ParsedSection;
- index: number;
- isDarkMode?: boolean;
- sectionOverrides?: Record>;
-}
-
-/**
- * Renders individual markdown sections with smart template detection
- * Uses specialized components for known PRP templates, beautiful styling for generic sections
- */
-export const MarkdownSectionRenderer: React.FC = ({
- section,
- index,
- isDarkMode = false,
- sectionOverrides = {}
-}) => {
- // If section matches a known PRP template, use the specialized component
- if (section.templateType) {
- const { type } = detectSectionType(section.sectionKey, section.rawContent);
-
- // Use the existing SectionRenderer with the detected type
- return (
-
-
-
- );
- }
-
- // For generic sections, render with beautiful floating styling
- return (
-
-
- {/* Section Header */}
-
-
- {section.title}
-
-
-
-
- {/* Section Content */}
-
- {/* Subtle background for sections with complex content */}
- {(section.type === 'code' || section.type === 'mixed') && (
-
- )}
-
-
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/components/SimpleMarkdown.tsx b/archon-ui-main/src/components/prp/components/SimpleMarkdown.tsx
deleted file mode 100644
index bf59aabc24..0000000000
--- a/archon-ui-main/src/components/prp/components/SimpleMarkdown.tsx
+++ /dev/null
@@ -1,340 +0,0 @@
-import React from 'react';
-import { formatValue } from '../utils/formatters';
-
-interface SimpleMarkdownProps {
- content: string;
- className?: string;
-}
-
-/**
- * Simple markdown renderer that handles basic formatting without external dependencies
- */
-export const SimpleMarkdown: React.FC = ({ content, className = '' }) => {
- try {
- // Process image placeholders first
- const processedContent = formatValue(content);
-
- // Split content into lines for processing
- const lines = processedContent.split('\n');
- const elements: React.ReactNode[] = [];
- let currentList: string[] = [];
- let listType: 'ul' | 'ol' | null = null;
-
- const flushList = () => {
- if (currentList.length > 0 && listType) {
- const ListComponent = listType === 'ul' ? 'ul' : 'ol';
- elements.push(
-
-
- {currentList.map((item, idx) => (
- {processInlineMarkdown(item)}
- ))}
-
-
- );
- currentList = [];
- listType = null;
- }
- };
-
- const processInlineMarkdown = (text: string): React.ReactNode => {
- const processed = text;
- const elements: React.ReactNode[] = [];
- let lastIndex = 0;
-
- // Process **bold** text
- const boldRegex = /\*\*(.*?)\*\*/g;
- let match;
- while ((match = boldRegex.exec(processed)) !== null) {
- if (match.index > lastIndex) {
- elements.push(processed.slice(lastIndex, match.index));
- }
- elements.push({match[1]} );
- lastIndex = match.index + match[0].length;
- }
-
- // Process *italic* text
- const italicRegex = /\*(.*?)\*/g;
- const remainingText = processed.slice(lastIndex);
- lastIndex = 0;
- const italicElements: React.ReactNode[] = [];
-
- while ((match = italicRegex.exec(remainingText)) !== null) {
- if (match.index > lastIndex) {
- italicElements.push(remainingText.slice(lastIndex, match.index));
- }
- italicElements.push({match[1]} );
- lastIndex = match.index + match[0].length;
- }
-
- if (lastIndex < remainingText.length) {
- italicElements.push(remainingText.slice(lastIndex));
- }
-
- if (elements.length > 0) {
- elements.push(...italicElements);
- return <>{elements}>;
- }
-
- if (italicElements.length > 0) {
- return <>{italicElements}>;
- }
-
- // Process `inline code`
- const codeRegex = /`([^`]+)`/g;
- const parts = text.split(codeRegex);
- if (parts.length > 1) {
- return (
- <>
- {parts.map((part, index) =>
- index % 2 === 0 ? (
- part
- ) : (
-
- {part}
-
- )
- )}
- >
- );
- }
-
- return {text} ;
- };
-
- let inCodeBlock = false;
- let codeBlockContent: string[] = [];
- let codeBlockLanguage = '';
- let inTable = false;
- let tableRows: string[][] = [];
- let tableHeaders: string[] = [];
-
- const flushTable = () => {
- if (tableRows.length > 0) {
- elements.push(
-
-
-
- {tableHeaders.length > 0 && (
-
-
- {tableHeaders.map((header, idx) => (
-
- {processInlineMarkdown(header.trim())}
-
- ))}
-
-
- )}
-
- {tableRows.map((row, rowIdx) => (
-
- {row.map((cell, cellIdx) => (
-
- {processInlineMarkdown(cell.trim())}
-
- ))}
-
- ))}
-
-
-
-
- );
- tableRows = [];
- tableHeaders = [];
- inTable = false;
- }
- };
-
- lines.forEach((line, index) => {
- // Handle code block start/end
- if (line.startsWith('```')) {
- if (!inCodeBlock) {
- // Starting code block
- flushList();
- inCodeBlock = true;
- codeBlockLanguage = line.substring(3).trim();
- codeBlockContent = [];
- } else {
- // Ending code block
- inCodeBlock = false;
- elements.push(
-
- {codeBlockLanguage && (
-
- {codeBlockLanguage}
-
- )}
-
-
- {codeBlockContent.join('\n')}
-
-
-
- );
- codeBlockContent = [];
- codeBlockLanguage = '';
- }
- return;
- }
-
- // If inside code block, collect content
- if (inCodeBlock) {
- codeBlockContent.push(line);
- return;
- }
-
- // Handle table rows
- if (line.includes('|') && line.trim() !== '') {
- const cells = line.split('|').map(cell => cell.trim()).filter(cell => cell !== '');
-
- if (cells.length > 0) {
- if (!inTable) {
- // Starting a new table
- flushList();
- inTable = true;
- tableHeaders = cells;
- } else if (cells.every(cell => cell.match(/^:?-+:?$/))) {
- // This is a header separator line (|---|---|), skip it
- return;
- } else {
- // This is a regular table row
- tableRows.push(cells);
- }
- return;
- }
- } else if (inTable) {
- // End of table (empty line or non-table content)
- flushTable();
- }
-
- // Handle headings
- const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
- if (headingMatch) {
- flushList();
- const level = headingMatch[1].length;
- const text = headingMatch[2];
- const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
- const sizeClasses = ['text-2xl', 'text-xl', 'text-lg', 'text-base', 'text-sm', 'text-xs'];
- const colorClasses = ['text-gray-900 dark:text-white', 'text-gray-800 dark:text-gray-100', 'text-gray-700 dark:text-gray-200', 'text-gray-700 dark:text-gray-200', 'text-gray-600 dark:text-gray-300', 'text-gray-600 dark:text-gray-300'];
-
- elements.push(
-
- {processInlineMarkdown(text)}
-
- );
- return;
- }
-
- // Handle checkboxes (task lists)
- const checkboxMatch = line.match(/^[-*+]\s+\[([ x])\]\s+(.+)$/);
- if (checkboxMatch) {
- flushList();
- const isChecked = checkboxMatch[1] === 'x';
- const content = checkboxMatch[2];
- elements.push(
-
-
- {isChecked && (
-
-
-
- )}
-
-
- {processInlineMarkdown(content)}
-
-
- );
- return;
- }
-
- // Handle bullet lists
- const bulletMatch = line.match(/^[-*+]\s+(.+)$/);
- if (bulletMatch) {
- if (listType !== 'ul') {
- flushList();
- listType = 'ul';
- }
- currentList.push(bulletMatch[1]);
- return;
- }
-
- // Handle numbered lists
- const numberMatch = line.match(/^\d+\.\s+(.+)$/);
- if (numberMatch) {
- if (listType !== 'ol') {
- flushList();
- listType = 'ol';
- }
- currentList.push(numberMatch[1]);
- return;
- }
-
- // Handle code blocks
- if (line.startsWith('```')) {
- flushList();
- // Simple code block handling - just skip the backticks
- return;
- }
-
- // Handle blockquotes
- if (line.startsWith('>')) {
- flushList();
- const content = line.substring(1).trim();
- elements.push(
-
-
- {processInlineMarkdown(content)}
-
-
- );
- return;
- }
-
- // Handle horizontal rules
- if (line.match(/^(-{3,}|_{3,}|\*{3,})$/)) {
- flushList();
- elements.push( );
- return;
- }
-
- // Regular paragraph
- if (line.trim()) {
- flushList();
- elements.push(
-
- {processInlineMarkdown(line)}
-
- );
- }
- });
-
- // Flush any remaining list or table
- flushList();
- flushTable();
-
- return (
-
- );
- } catch (error) {
- console.error('Error rendering markdown:', error, content);
- return (
-
-
Error rendering markdown content
-
- {content}
-
-
- );
- }
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/index.ts b/archon-ui-main/src/components/prp/index.ts
deleted file mode 100644
index 0373c873f8..0000000000
--- a/archon-ui-main/src/components/prp/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// Main component exports
-export { PRPViewer } from './PRPViewer';
-
-// Section component exports
-export { MetadataSection } from './sections/MetadataSection';
-export { ContextSection } from './sections/ContextSection';
-export { PersonaSection } from './sections/PersonaSection';
-export { FlowSection } from './sections/FlowSection';
-export { MetricsSection } from './sections/MetricsSection';
-export { PlanSection } from './sections/PlanSection';
-export { ListSection } from './sections/ListSection';
-export { ObjectSection } from './sections/ObjectSection';
-export { KeyValueSection } from './sections/KeyValueSection';
-export { FeatureSection } from './sections/FeatureSection';
-export { GenericSection } from './sections/GenericSection';
-
-// Renderer exports
-export { SectionRenderer } from './renderers/SectionRenderer';
-
-// Type exports
-export * from './types/prp.types';
-
-// Utility exports
-export { detectSectionType, formatSectionTitle, getSectionIcon } from './utils/sectionDetector';
-export { formatKey, formatValue, truncateText, getAccentColor } from './utils/formatters';
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/renderers/SectionRenderer.tsx b/archon-ui-main/src/components/prp/renderers/SectionRenderer.tsx
deleted file mode 100644
index ad9b99b8a9..0000000000
--- a/archon-ui-main/src/components/prp/renderers/SectionRenderer.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import React from 'react';
-import {
- Brain, Users, Workflow, BarChart3, Clock, Shield,
- Code, Layers, FileText, List, Hash, Box
-} from 'lucide-react';
-import { detectSectionType, formatSectionTitle } from '../utils/sectionDetector';
-import { getAccentColor } from '../utils/formatters';
-
-// Import all section components
-import { ContextSection } from '../sections/ContextSection';
-import { PersonaSection } from '../sections/PersonaSection';
-import { FlowSection } from '../sections/FlowSection';
-import { MetricsSection } from '../sections/MetricsSection';
-import { PlanSection } from '../sections/PlanSection';
-import { ListSection } from '../sections/ListSection';
-import { ObjectSection } from '../sections/ObjectSection';
-import { KeyValueSection } from '../sections/KeyValueSection';
-import { FeatureSection } from '../sections/FeatureSection';
-import { GenericSection } from '../sections/GenericSection';
-import { RolloutPlanSection } from '../sections/RolloutPlanSection';
-import { TokenSystemSection } from '../sections/TokenSystemSection';
-
-interface SectionRendererProps {
- sectionKey: string;
- data: any;
- index: number;
- isDarkMode?: boolean;
- sectionOverrides?: Record>;
-}
-
-/**
- * Dynamically renders sections based on their type
- */
-export const SectionRenderer: React.FC = ({
- sectionKey,
- data,
- index,
- isDarkMode = false,
- sectionOverrides = {},
-}) => {
- // Skip metadata fields (handled by MetadataSection)
- const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type'];
- if (metadataFields.includes(sectionKey)) {
- return null;
- }
-
- // Check for custom override first
- if (sectionOverrides[sectionKey]) {
- const CustomComponent = sectionOverrides[sectionKey];
- return ;
- }
-
- // Detect section type
- const { type } = detectSectionType(sectionKey, data);
-
- // Get appropriate icon based on section key
- const getIcon = () => {
- const normalizedKey = sectionKey.toLowerCase();
- if (normalizedKey.includes('context') || normalizedKey.includes('overview')) return ;
- if (normalizedKey.includes('persona') || normalizedKey.includes('user')) return ;
- if (normalizedKey.includes('flow') || normalizedKey.includes('journey')) return ;
- if (normalizedKey.includes('metric') || normalizedKey.includes('success')) return ;
- if (normalizedKey.includes('plan') || normalizedKey.includes('implementation')) return ;
- if (normalizedKey.includes('validation') || normalizedKey.includes('gate')) return ;
- if (normalizedKey.includes('technical') || normalizedKey.includes('tech')) return ;
- if (normalizedKey.includes('architecture')) return ;
- if (Array.isArray(data)) return
;
- if (typeof data === 'object') return ;
- return ;
- };
-
- // Get accent color based on section or index
- const getColor = () => {
- const normalizedKey = sectionKey.toLowerCase();
- if (normalizedKey.includes('context')) return 'blue';
- if (normalizedKey.includes('persona')) return 'purple';
- if (normalizedKey.includes('flow') || normalizedKey.includes('journey')) return 'orange';
- if (normalizedKey.includes('metric')) return 'green';
- if (normalizedKey.includes('plan')) return 'cyan';
- if (normalizedKey.includes('validation')) return 'emerald';
- return getAccentColor(index);
- };
-
- const commonProps = {
- title: formatSectionTitle(sectionKey),
- data,
- icon: getIcon(),
- accentColor: getColor(),
- isDarkMode,
- defaultOpen: index < 5, // Open first 5 sections by default
- isCollapsible: true, // Make all sections collapsible by default
- };
-
- // Check for specific section types by key name first
- const normalizedKey = sectionKey.toLowerCase();
-
- // Special handling for rollout plans
- if (normalizedKey.includes('rollout') || normalizedKey === 'rollout_plan') {
- return ;
- }
-
- // Special handling for token systems
- if (normalizedKey.includes('token') || normalizedKey === 'token_system' ||
- normalizedKey === 'design_tokens' || normalizedKey === 'design_system') {
- return ;
- }
-
- // Render based on detected type
- switch (type) {
- case 'context':
- return ;
-
- case 'personas':
- return ;
-
- case 'flows':
- return ;
-
- case 'metrics':
- return ;
-
- case 'plan':
- return ;
-
- case 'list':
- return ;
-
- case 'keyvalue':
- return ;
-
- case 'object':
- return ;
-
- case 'features':
- return ;
-
- case 'generic':
- default:
- return ;
- }
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/ContextSection.tsx b/archon-ui-main/src/components/prp/sections/ContextSection.tsx
deleted file mode 100644
index ebdd788ca1..0000000000
--- a/archon-ui-main/src/components/prp/sections/ContextSection.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from 'react';
-import { Target, BookOpen, Sparkles, CheckCircle2 } from 'lucide-react';
-import { SectionProps } from '../types/prp.types';
-// Temporarily disabled to debug black screen issue
-// import { renderValue, renderValueInline } from '../utils/objectRenderer';
-
-/**
- * Renders context sections like scope, background, objectives
- */
-export const ContextSection: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'blue',
- defaultOpen = true,
- isDarkMode = false,
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const renderContextItem = (key: string, value: any) => {
- const getItemIcon = (itemKey: string) => {
- const normalizedKey = itemKey.toLowerCase();
- if (normalizedKey.includes('scope')) return ;
- if (normalizedKey.includes('background')) return ;
- if (normalizedKey.includes('objective')) return ;
- if (normalizedKey.includes('requirement')) return ;
- return ;
- };
-
- const getItemColor = (itemKey: string) => {
- const normalizedKey = itemKey.toLowerCase();
- if (normalizedKey.includes('scope')) return 'blue';
- if (normalizedKey.includes('background')) return 'purple';
- if (normalizedKey.includes('objective')) return 'green';
- if (normalizedKey.includes('requirement')) return 'orange';
- return 'gray';
- };
-
- const color = getItemColor(key);
- const colorMap = {
- blue: 'bg-blue-50/50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
- purple: 'bg-purple-50/50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800',
- green: 'bg-green-50/50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
- orange: 'bg-orange-50/50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800',
- gray: 'bg-gray-50/50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800',
- };
-
- const itemTitle = key.replace(/_/g, ' ').charAt(0).toUpperCase() + key.replace(/_/g, ' ').slice(1);
-
- return (
-
-
- {getItemIcon(key)}
- {itemTitle}
-
-
- {Array.isArray(value) ? (
-
- {value.map((item: any, idx: number) => (
-
-
- {typeof item === 'string' ? item : JSON.stringify(item)}
-
- ))}
-
- ) : typeof value === 'string' ? (
-
{value}
- ) : (
-
- {JSON.stringify(value, null, 2)}
-
- )}
-
- );
- };
-
- return (
-
- {Object.entries(data).map(([key, value]) => renderContextItem(key, value))}
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/FeatureSection.tsx b/archon-ui-main/src/components/prp/sections/FeatureSection.tsx
deleted file mode 100644
index ee30f3a9e3..0000000000
--- a/archon-ui-main/src/components/prp/sections/FeatureSection.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React from 'react';
-import { Package, Star, FileText } from 'lucide-react';
-import { PRPSectionProps } from '../types/prp.types';
-import { formatKey, formatValue } from '../utils/formatters';
-
-/**
- * Specialized component for feature requirements and capabilities
- * Renders features in organized categories with proper hierarchy
- */
-export const FeatureSection: React.FC = ({
- title,
- data,
- icon = ,
- accentColor = 'blue',
- isDarkMode = false,
- defaultOpen = true
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const colorMap = {
- blue: 'from-blue-400 to-blue-600',
- purple: 'from-purple-400 to-purple-600',
- green: 'from-green-400 to-green-600',
- orange: 'from-orange-400 to-orange-600',
- pink: 'from-pink-400 to-pink-600',
- cyan: 'from-cyan-400 to-cyan-600',
- indigo: 'from-indigo-400 to-indigo-600',
- emerald: 'from-emerald-400 to-emerald-600',
- };
-
- const bgColorMap = {
- blue: 'bg-blue-50 dark:bg-blue-950',
- purple: 'bg-purple-50 dark:bg-purple-950',
- green: 'bg-green-50 dark:bg-green-950',
- orange: 'bg-orange-50 dark:bg-orange-950',
- pink: 'bg-pink-50 dark:bg-pink-950',
- cyan: 'bg-cyan-50 dark:bg-cyan-950',
- indigo: 'bg-indigo-50 dark:bg-indigo-950',
- emerald: 'bg-emerald-50 dark:bg-emerald-950',
- };
-
- const renderFeatureGroup = (groupName: string, features: any, isPremium: boolean = false) => {
- if (!features || typeof features !== 'object') return null;
-
- const IconComponent = isPremium ? Star : FileText;
- const iconColor = isPremium ? 'text-yellow-500' : 'text-blue-500';
-
- return (
-
-
-
-
- {formatKey(groupName)}
-
- {isPremium && (
-
- Premium
-
- )}
-
-
-
- {Object.entries(features).map(([featureName, featureData]) => (
-
-
- {formatKey(featureName)}
-
-
- {Array.isArray(featureData) ? (
-
- {featureData.map((item, index) => (
-
- •
- {formatValue(item)}
-
- ))}
-
- ) : typeof featureData === 'string' ? (
-
{featureData}
- ) : (
-
- {formatValue(featureData)}
-
- )}
-
- ))}
-
-
- );
- };
-
- const renderFeatureList = (features: any) => {
- if (Array.isArray(features)) {
- return (
-
- {features.map((feature, index) => (
-
-
- {formatValue(feature)}
-
- ))}
-
- );
- }
-
- if (typeof features === 'object' && features !== null) {
- return (
-
- {Object.entries(features).map(([key, value]) => {
- const isPremium = key.toLowerCase().includes('premium') ||
- key.toLowerCase().includes('advanced') ||
- key.toLowerCase().includes('pro');
-
- if (typeof value === 'object' && value !== null) {
- return renderFeatureGroup(key, value, isPremium);
- }
-
- return (
-
-
-
-
- {formatKey(key)}
-
-
- {formatValue(value)}
-
-
-
- );
- })}
-
- );
- }
-
- return {formatValue(features)}
;
- };
-
- return (
-
-
-
-
- {icon}
-
-
- {title}
-
-
-
- {renderFeatureList(data)}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/FlowSection.tsx b/archon-ui-main/src/components/prp/sections/FlowSection.tsx
deleted file mode 100644
index cbb96dbc7e..0000000000
--- a/archon-ui-main/src/components/prp/sections/FlowSection.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import { Workflow, Navigation } from 'lucide-react';
-import { SectionProps } from '../types/prp.types';
-import { formatKey } from '../utils/formatters';
-
-/**
- * Renders user flows and journey diagrams
- */
-export const FlowSection: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'orange',
- defaultOpen = true,
- isDarkMode = false,
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const renderFlowNode = (obj: any, depth: number = 0): React.ReactNode => {
- if (!obj || typeof obj !== 'object') {
- return {String(obj)} ;
- }
-
- return Object.entries(obj).map(([key, value]) => {
- const nodeKey = `${key}-${depth}-${Math.random()}`;
-
- if (typeof value === 'string') {
- return (
-
-
-
- {formatKey(key)}:
-
-
{value}
-
- );
- } else if (typeof value === 'object' && value !== null) {
- return (
-
-
-
- {formatKey(key)}
-
-
- {renderFlowNode(value, depth + 1)}
-
-
- );
- }
- return null;
- });
- };
-
- return (
-
- {Object.entries(data).map(([flowName, flow]) => (
-
-
-
- {formatKey(flowName)}
-
-
- {renderFlowNode(flow)}
-
-
- ))}
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/GenericSection.tsx b/archon-ui-main/src/components/prp/sections/GenericSection.tsx
deleted file mode 100644
index ed3f5a72df..0000000000
--- a/archon-ui-main/src/components/prp/sections/GenericSection.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import React from 'react';
-import { FileText, Hash, List, Box, Type, ToggleLeft } from 'lucide-react';
-import { SectionProps } from '../types/prp.types';
-import { formatKey, formatValue } from '../utils/formatters';
-import { hasComplexNesting } from '../utils/normalizer';
-import { CollapsibleSectionWrapper } from '../components/CollapsibleSectionWrapper';
-import { SimpleMarkdown } from '../components/SimpleMarkdown';
-
-/**
- * Generic fallback section component that intelligently renders any data structure
- * This component provides comprehensive rendering for any data type with proper formatting
- */
-export const GenericSection: React.FC = ({
- title,
- data,
- icon = ,
- accentColor = 'gray',
- defaultOpen = true,
- isDarkMode = false,
- isCollapsible = true,
- isOpen,
- onToggle
-}) => {
- // Auto-detect appropriate icon based on data type
- const getAutoIcon = () => {
- if (typeof data === 'string') return ;
- if (typeof data === 'number') return ;
- if (typeof data === 'boolean') return ;
- if (Array.isArray(data)) return
;
- if (typeof data === 'object' && data !== null) return ;
- return icon;
- };
- const renderValue = (value: any, depth: number = 0): React.ReactNode => {
- const indent = depth * 16;
- const maxDepth = 5; // Prevent infinite recursion
-
- // Handle null/undefined
- if (value === null || value === undefined) {
- return Empty ;
- }
-
- // Handle primitives
- if (typeof value === 'string') {
- // Check if the string looks like markdown content
- const hasMarkdownIndicators = /^#{1,6}\s+.+$|^[-*+]\s+.+$|^\d+\.\s+.+$|```|^\>.+$|\*\*.+\*\*|\*.+\*|`[^`]+`/m.test(value);
-
- if (hasMarkdownIndicators && value.length > 20) {
- // Render as markdown for content with markdown syntax
- // Remove any leading headers since the section already has a title
- const contentWithoutLeadingHeaders = value.replace(/^#{1,6}\s+.+$/m, '').trim();
- const finalContent = contentWithoutLeadingHeaders || value;
-
- return ;
- }
-
- // For shorter strings or non-markdown, use simple formatting
- return {formatValue(value)} ;
- }
-
- if (typeof value === 'number' || typeof value === 'boolean') {
- return {formatValue(value)} ;
- }
-
- // Prevent deep recursion
- if (depth >= maxDepth) {
- return (
-
- [Complex nested structure - too deep to display]
-
- );
- }
-
- // Handle arrays
- if (Array.isArray(value)) {
- if (value.length === 0) {
- return No items ;
- }
-
- // Check if it's an array of primitives
- const isSimpleArray = value.every(item =>
- typeof item === 'string' ||
- typeof item === 'number' ||
- typeof item === 'boolean' ||
- item === null ||
- item === undefined
- );
-
- if (isSimpleArray) {
- // For very long arrays, show first 10 and count
- const displayItems = value.length > 10 ? value.slice(0, 10) : value;
- const hasMore = value.length > 10;
-
- return (
-
-
- {displayItems.map((item, index) => (
-
- •
- {formatValue(item)}
-
- ))}
-
- {hasMore && (
-
- ... and {value.length - 10} more items
-
- )}
-
- );
- }
-
- // Array of objects
- const displayItems = value.length > 5 ? value.slice(0, 5) : value;
- const hasMore = value.length > 5;
-
- return (
-
- {displayItems.map((item, index) => (
-
-
-
-
- [{index}]
-
- {renderValue(item, depth + 1)}
-
-
- ))}
- {hasMore && (
-
- ... and {value.length - 5} more items
-
- )}
-
- );
- }
-
- // Handle objects
- if (typeof value === 'object' && value !== null) {
- // Simplified object rendering to debug black screen
- return (
-
-
- {JSON.stringify(value, null, 2)}
-
-
- );
- }
-
- // Fallback for any other type (functions, symbols, etc.)
- return (
-
- [{typeof value}]
-
- );
- };
-
- const getBackgroundColor = () => {
- const colorMap = {
- blue: 'bg-blue-50/50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
- purple: 'bg-purple-50/50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800',
- green: 'bg-green-50/50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
- orange: 'bg-orange-50/50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800',
- pink: 'bg-pink-50/50 dark:bg-pink-900/20 border-pink-200 dark:border-pink-800',
- cyan: 'bg-cyan-50/50 dark:bg-cyan-900/20 border-cyan-200 dark:border-cyan-800',
- gray: 'bg-gray-50/50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800',
- };
- return colorMap[accentColor as keyof typeof colorMap] || colorMap.gray;
- };
-
- const finalIcon = icon === ? getAutoIcon() : icon;
-
- // Enhanced styling based on data complexity
- const isComplexData = hasComplexNesting(data);
- const headerClass = isComplexData
- ? `p-6 rounded-lg border-2 shadow-sm ${getBackgroundColor()}`
- : `p-4 rounded-lg border ${getBackgroundColor()}`;
-
- const header = (
-
-
-
- {finalIcon}
-
- {title}
-
-
- );
-
- const contentClass = isComplexData
- ? `px-6 pb-6 -mt-1 rounded-b-lg border-2 border-t-0 shadow-sm ${getBackgroundColor()}`
- : `px-4 pb-4 -mt-1 rounded-b-lg border border-t-0 ${getBackgroundColor()}`;
-
- const content = (
-
-
- {/* Add a subtle background for complex data */}
- {isComplexData ? (
-
- {renderValue(data)}
-
- ) : (
- renderValue(data)
- )}
-
-
- );
-
- try {
- return (
-
- {content}
-
- );
- } catch (error) {
- console.error('Error rendering GenericSection:', error, { title, data });
- return (
-
-
{title}
-
Error rendering section content
-
- {JSON.stringify(data, null, 2)}
-
-
- );
- }
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/KeyValueSection.tsx b/archon-ui-main/src/components/prp/sections/KeyValueSection.tsx
deleted file mode 100644
index c8fa750476..0000000000
--- a/archon-ui-main/src/components/prp/sections/KeyValueSection.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import React from 'react';
-import { Hash } from 'lucide-react';
-import { PRPSectionProps } from '../types/prp.types';
-import { formatKey, formatValue } from '../utils/formatters';
-
-/**
- * Component for rendering simple key-value pairs
- * Used for sections like budget, resources, team, etc.
- */
-export const KeyValueSection: React.FC = ({
- title,
- data,
- icon = ,
- accentColor = 'green',
- isDarkMode = false,
- defaultOpen = true
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const colorMap = {
- blue: 'from-blue-400 to-blue-600',
- purple: 'from-purple-400 to-purple-600',
- green: 'from-green-400 to-green-600',
- orange: 'from-orange-400 to-orange-600',
- pink: 'from-pink-400 to-pink-600',
- cyan: 'from-cyan-400 to-cyan-600',
- indigo: 'from-indigo-400 to-indigo-600',
- emerald: 'from-emerald-400 to-emerald-600',
- };
-
- const borderColorMap = {
- blue: 'border-blue-200 dark:border-blue-800',
- purple: 'border-purple-200 dark:border-purple-800',
- green: 'border-green-200 dark:border-green-800',
- orange: 'border-orange-200 dark:border-orange-800',
- pink: 'border-pink-200 dark:border-pink-800',
- cyan: 'border-cyan-200 dark:border-cyan-800',
- indigo: 'border-indigo-200 dark:border-indigo-800',
- emerald: 'border-emerald-200 dark:border-emerald-800',
- };
-
- const renderValue = (value: any): React.ReactNode => {
- if (Array.isArray(value)) {
- return (
-
- {value.map((item, index) => (
-
- {formatValue(item)}
-
- ))}
-
- );
- }
-
- if (typeof value === 'object' && value !== null) {
- return (
-
- {Object.entries(value).map(([k, v]) => (
-
-
- {formatKey(k)}
-
-
- {formatValue(v)}
-
-
- ))}
-
- );
- }
-
- return (
-
- {formatValue(value)}
-
- );
- };
-
- return (
-
-
-
-
- {icon}
-
-
- {title}
-
-
-
-
- {Object.entries(data).map(([key, value]) => (
-
-
-
- {formatKey(key)}
-
-
- {renderValue(value)}
-
-
-
- ))}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/ListSection.tsx b/archon-ui-main/src/components/prp/sections/ListSection.tsx
deleted file mode 100644
index e557878a54..0000000000
--- a/archon-ui-main/src/components/prp/sections/ListSection.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from 'react';
-import { CheckCircle2, Circle } from 'lucide-react';
-import { SectionProps } from '../types/prp.types';
-
-/**
- * Renders simple list/array data
- */
-export const ListSection: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'green',
- defaultOpen = true,
- isDarkMode = false,
-}) => {
- if (!Array.isArray(data)) return null;
-
- const getItemIcon = (item: any, index: number) => {
- // Use checkmarks for validation/success items
- if (title.toLowerCase().includes('validation') ||
- title.toLowerCase().includes('success') ||
- title.toLowerCase().includes('complete')) {
- return ;
- }
- // Use circles for general items
- return ;
- };
-
- const getBackgroundColor = () => {
- const colorMap = {
- green: 'bg-gradient-to-br from-green-50/50 to-emerald-50/50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800',
- blue: 'bg-gradient-to-br from-blue-50/50 to-cyan-50/50 dark:from-blue-900/20 dark:to-cyan-900/20 border-blue-200 dark:border-blue-800',
- purple: 'bg-gradient-to-br from-purple-50/50 to-pink-50/50 dark:from-purple-900/20 dark:to-pink-900/20 border-purple-200 dark:border-purple-800',
- orange: 'bg-gradient-to-br from-orange-50/50 to-yellow-50/50 dark:from-orange-900/20 dark:to-yellow-900/20 border-orange-200 dark:border-orange-800',
- gray: 'bg-gradient-to-br from-gray-50/50 to-slate-50/50 dark:from-gray-900/20 dark:to-slate-900/20 border-gray-200 dark:border-gray-800',
- };
- return colorMap[accentColor as keyof typeof colorMap] || colorMap.gray;
- };
-
- if (data.length === 0) {
- return (
-
- );
- }
-
- return (
-
-
- {data.map((item: any, idx: number) => (
-
- {getItemIcon(item, idx)}
-
- {typeof item === 'string' ? (
-
{item}
- ) : typeof item === 'object' && item !== null ? (
-
- {Object.entries(item).map(([key, value]) => (
-
-
- {key.replace(/_/g, ' ')}:
-
-
- {typeof value === 'string' ? value : JSON.stringify(value)}
-
-
- ))}
-
- ) : (
-
{String(item)}
- )}
-
-
- ))}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/MetadataSection.tsx b/archon-ui-main/src/components/prp/sections/MetadataSection.tsx
deleted file mode 100644
index a4c2836aa1..0000000000
--- a/archon-ui-main/src/components/prp/sections/MetadataSection.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React from 'react';
-import { Award, Users, Clock, Tag, FileText } from 'lucide-react';
-import { PRPContent } from '../types/prp.types';
-
-interface MetadataSectionProps {
- content: PRPContent;
- isDarkMode?: boolean;
-}
-
-/**
- * Renders the metadata header section of a PRP document
- */
-export const MetadataSection: React.FC = ({ content, isDarkMode = false }) => {
- const getIcon = (field: string) => {
- switch (field) {
- case 'version': return ;
- case 'author': return ;
- case 'date': return ;
- case 'status': return ;
- default: return ;
- }
- };
-
- const formatStatus = (status: string) => {
- const statusColors = {
- draft: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
- review: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
- approved: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
- published: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
- };
-
- const colorClass = statusColors[status.toLowerCase() as keyof typeof statusColors] ||
- 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
-
- return (
-
- {status.charAt(0).toUpperCase() + status.slice(1)}
-
- );
- };
-
- const metadataFields = ['version', 'author', 'date', 'status'];
- const hasMetadata = metadataFields.some(field => content[field]);
-
- if (!hasMetadata && !content.title) {
- return null;
- }
-
- return (
-
-
- {content.title || 'Product Requirements Prompt'}
-
-
-
- {metadataFields.map(field => {
- const value = content[field];
- if (!value) return null;
-
- return (
-
- {getIcon(field)}
- {field === 'status' ? (
- formatStatus(value)
- ) : (
-
- {field === 'version' && 'Version'} {value}
-
- )}
-
- );
- })}
-
- {content.document_type && (
-
-
-
- {content.document_type}
-
-
- )}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/MetricsSection.tsx b/archon-ui-main/src/components/prp/sections/MetricsSection.tsx
deleted file mode 100644
index 18e91a8d91..0000000000
--- a/archon-ui-main/src/components/prp/sections/MetricsSection.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from 'react';
-import { BarChart3, Settings, Users, Gauge } from 'lucide-react';
-import { SectionProps } from '../types/prp.types';
-import { formatKey } from '../utils/formatters';
-
-/**
- * Renders success metrics and KPIs
- */
-export const MetricsSection: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'green',
- defaultOpen = true,
- isDarkMode = false,
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const getCategoryColor = (category: string): string => {
- const normalizedCategory = category.toLowerCase();
- if (normalizedCategory.includes('admin')) return 'from-blue-400 to-blue-600';
- if (normalizedCategory.includes('business')) return 'from-purple-400 to-purple-600';
- if (normalizedCategory.includes('customer')) return 'from-green-400 to-green-600';
- if (normalizedCategory.includes('technical')) return 'from-orange-400 to-orange-600';
- if (normalizedCategory.includes('performance')) return 'from-red-400 to-red-600';
- return 'from-gray-400 to-gray-600';
- };
-
- const getCategoryIcon = (category: string): React.ReactNode => {
- const normalizedCategory = category.toLowerCase();
- if (normalizedCategory.includes('admin')) return ;
- if (normalizedCategory.includes('business')) return ;
- if (normalizedCategory.includes('customer')) return ;
- return ;
- };
-
- const renderMetric = (metric: string, category: string, index: number) => {
- return (
-
-
- {getCategoryIcon(category)}
-
-
{metric}
-
- );
- };
-
- return (
-
- {Object.entries(data).map(([category, metrics]: [string, any]) => (
-
-
- {formatKey(category)}
-
-
- {Array.isArray(metrics) ?
- metrics.map((metric: string, idx: number) =>
- renderMetric(metric, category, idx)
- ) :
- typeof metrics === 'object' && metrics !== null ?
- Object.entries(metrics).map(([key, value], idx) =>
- renderMetric(`${formatKey(key)}: ${value}`, category, idx)
- ) :
- renderMetric(String(metrics), category, 0)
- }
-
-
- ))}
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/ObjectSection.tsx b/archon-ui-main/src/components/prp/sections/ObjectSection.tsx
deleted file mode 100644
index 449d2456f7..0000000000
--- a/archon-ui-main/src/components/prp/sections/ObjectSection.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import React from 'react';
-import { Box, FileText } from 'lucide-react';
-import { PRPSectionProps } from '../types/prp.types';
-import { formatKey, formatValue } from '../utils/formatters';
-import { CollapsibleSectionWrapper } from '../components/CollapsibleSectionWrapper';
-
-/**
- * Component for rendering complex object structures with nested data
- * Used for sections like design systems, architecture, etc.
- */
-export const ObjectSection: React.FC = ({
- title,
- data,
- icon = ,
- accentColor = 'indigo',
- isDarkMode = false,
- defaultOpen = true,
- isCollapsible = true,
- isOpen,
- onToggle
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const colorMap = {
- blue: 'from-blue-400 to-blue-600 border-blue-500',
- purple: 'from-purple-400 to-purple-600 border-purple-500',
- green: 'from-green-400 to-green-600 border-green-500',
- orange: 'from-orange-400 to-orange-600 border-orange-500',
- pink: 'from-pink-400 to-pink-600 border-pink-500',
- cyan: 'from-cyan-400 to-cyan-600 border-cyan-500',
- indigo: 'from-indigo-400 to-indigo-600 border-indigo-500',
- emerald: 'from-emerald-400 to-emerald-600 border-emerald-500',
- };
-
- const bgColorMap = {
- blue: 'bg-blue-50 dark:bg-blue-950',
- purple: 'bg-purple-50 dark:bg-purple-950',
- green: 'bg-green-50 dark:bg-green-950',
- orange: 'bg-orange-50 dark:bg-orange-950',
- pink: 'bg-pink-50 dark:bg-pink-950',
- cyan: 'bg-cyan-50 dark:bg-cyan-950',
- indigo: 'bg-indigo-50 dark:bg-indigo-950',
- emerald: 'bg-emerald-50 dark:bg-emerald-950',
- };
-
- const renderNestedObject = (obj: any, depth: number = 0): React.ReactNode => {
- if (!obj || typeof obj !== 'object') {
- return {formatValue(obj)} ;
- }
-
- if (Array.isArray(obj)) {
- // Handle empty arrays
- if (obj.length === 0) {
- return No items ;
- }
-
- // Check if it's a simple array (strings/numbers/booleans)
- const isSimpleArray = obj.every(item =>
- typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
- );
-
- if (isSimpleArray) {
- return (
-
- {obj.map((item, index) => (
-
- •
- {String(item)}
-
- ))}
-
- );
- }
-
- // Complex array with objects
- return (
-
- {obj.map((item, index) => (
-
0 ? 'border-l-2 border-gray-200 dark:border-gray-700 pl-4' : ''}`}>
-
Item {index + 1}
- {renderNestedObject(item, depth + 1)}
-
- ))}
-
- );
- }
-
- // Handle objects
- const entries = Object.entries(obj);
-
- // Group entries by type for better organization
- const stringEntries = entries.filter(([_, v]) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean');
- const arrayEntries = entries.filter(([_, v]) => Array.isArray(v));
- const objectEntries = entries.filter(([_, v]) => typeof v === 'object' && v !== null && !Array.isArray(v));
-
- return (
- 0 ? 'mt-2' : ''}`}>
- {/* Render simple key-value pairs first */}
- {stringEntries.length > 0 && (
-
0 ? 'ml-4' : ''} space-y-2`}>
- {stringEntries.map(([key, value]) => (
-
-
- {formatKey(key)}:
-
-
- {String(value)}
-
-
- ))}
-
- )}
-
- {/* Render arrays */}
- {arrayEntries.map(([key, value]) => (
-
0 ? 'ml-4' : ''}`}>
-
-
-
-
2 ? 'text-sm' : ''}`}>
- {formatKey(key)}
-
-
- {renderNestedObject(value, depth + 1)}
-
-
-
-
- ))}
-
- {/* Render nested objects */}
- {objectEntries.map(([key, value]) => {
- // Determine if this is a complex nested structure
- const isComplex = Object.values(value as object).some(v =>
- typeof v === 'object' && v !== null
- );
-
- return (
-
0 ? 'ml-4' : ''}`}>
-
1 ? 'mt-4' : ''}
- `}>
-
- {formatKey(key)}
-
-
2 ? 'text-xs' : 'text-sm'}>
- {renderNestedObject(value, depth + 1)}
-
-
-
- );
- })}
-
- );
- };
-
- const header = (
-
-
-
- {icon}
-
-
- {title}
-
-
-
- );
-
- const content = (
-
- {renderNestedObject(data)}
-
- );
-
- return (
-
-
- {content}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/PersonaSection.tsx b/archon-ui-main/src/components/prp/sections/PersonaSection.tsx
deleted file mode 100644
index 3bcd9120bc..0000000000
--- a/archon-ui-main/src/components/prp/sections/PersonaSection.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import React, { useState } from 'react';
-import { Target, Zap } from 'lucide-react';
-import { SectionProps, PRPPersona } from '../types/prp.types';
-
-/**
- * Renders user personas with expandable cards
- */
-export const PersonaSection: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'purple',
- defaultOpen = true,
- isDarkMode = false,
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- return (
-
- {Object.entries(data).map(([key, persona]) => (
-
- ))}
-
- );
-};
-
-interface PersonaCardProps {
- persona: PRPPersona;
- personaKey: string;
-}
-
-const PersonaCard: React.FC = ({ persona, personaKey }) => {
- const [isExpanded, setIsExpanded] = useState(false);
-
- const getPersonaIcon = (key: string) => {
- if (key.includes('admin')) return '👨💼';
- if (key.includes('formulator')) return '🧪';
- if (key.includes('purchasing')) return '💰';
- if (key.includes('developer')) return '👨💻';
- if (key.includes('designer')) return '🎨';
- if (key.includes('manager')) return '👔';
- if (key.includes('customer')) return '🛍️';
- return '👤';
- };
-
- const renderJourney = (journey: Record) => {
- return (
-
- {Object.entries(journey).map(([stage, description]) => (
-
-
- {stage}:
-
-
- {typeof description === 'string' ? description : JSON.stringify(description)}
-
-
- ))}
-
- );
- };
-
- const renderWorkflow = (workflow: Record) => {
- return (
-
- {Object.entries(workflow).map(([time, task]) => (
-
-
- {time}:
-
-
- {typeof task === 'string' ? task : JSON.stringify(task)}
-
-
- ))}
-
- );
- };
-
- return (
-
-
setIsExpanded(!isExpanded)}
- >
-
-
{getPersonaIcon(personaKey)}
-
-
- {persona.name || personaKey}
-
- {persona.role && (
-
{persona.role}
- )}
-
- {/* Always visible goals */}
- {persona.goals && Array.isArray(persona.goals) && persona.goals.length > 0 && (
-
-
-
- Goals
-
-
- {persona.goals.slice(0, isExpanded ? undefined : 2).map((goal: string, idx: number) => (
-
- •
- {goal}
-
- ))}
- {!isExpanded && persona.goals.length > 2 && (
-
- +{persona.goals.length - 2} more...
-
- )}
-
-
- )}
-
- {/* Expandable content */}
- {isExpanded && (
- <>
- {persona.pain_points && Array.isArray(persona.pain_points) && persona.pain_points.length > 0 && (
-
-
-
- Pain Points
-
-
- {persona.pain_points.map((point: string, idx: number) => (
-
- •
- {point}
-
- ))}
-
-
- )}
-
- {persona.journey && Object.keys(persona.journey).length > 0 && (
-
-
- User Journey
-
- {renderJourney(persona.journey)}
-
- )}
-
- {persona.workflow && Object.keys(persona.workflow).length > 0 && (
-
-
- Daily Workflow
-
- {renderWorkflow(persona.workflow)}
-
- )}
-
- {/* Render any other fields */}
- {Object.entries(persona).map(([key, value]) => {
- if (['name', 'role', 'goals', 'pain_points', 'journey', 'workflow'].includes(key)) {
- return null;
- }
- return (
-
-
- {key.replace(/_/g, ' ')}
-
-
- {typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
-
-
- );
- })}
- >
- )}
-
-
-
-
- Click to {isExpanded ? 'collapse' : 'expand'} details
-
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/PlanSection.tsx b/archon-ui-main/src/components/prp/sections/PlanSection.tsx
deleted file mode 100644
index 1caef30b0e..0000000000
--- a/archon-ui-main/src/components/prp/sections/PlanSection.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import React from 'react';
-import { Clock, Zap, CheckCircle2 } from 'lucide-react';
-import { SectionProps, PRPPhase } from '../types/prp.types';
-
-/**
- * Renders implementation plans and phases
- */
-export const PlanSection: React.FC = ({
- title,
- data,
- icon,
- accentColor = 'orange',
- defaultOpen = true,
- isDarkMode = false,
-}) => {
- if (!data || typeof data !== 'object') return null;
-
- const getPhaseColor = (index: number): string => {
- const colors = ['orange', 'yellow', 'green', 'blue', 'purple'];
- return colors[index % colors.length];
- };
-
- const renderPhase = (phaseKey: string, phase: PRPPhase, index: number) => {
- const color = getPhaseColor(index);
- const colorMap = {
- orange: 'from-orange-50/50 to-yellow-50/50 dark:from-orange-900/20 dark:to-yellow-900/20 border-orange-200 dark:border-orange-800',
- yellow: 'from-yellow-50/50 to-amber-50/50 dark:from-yellow-900/20 dark:to-amber-900/20 border-yellow-200 dark:border-yellow-800',
- green: 'from-green-50/50 to-emerald-50/50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800',
- blue: 'from-blue-50/50 to-cyan-50/50 dark:from-blue-900/20 dark:to-cyan-900/20 border-blue-200 dark:border-blue-800',
- purple: 'from-purple-50/50 to-pink-50/50 dark:from-purple-900/20 dark:to-pink-900/20 border-purple-200 dark:border-purple-800',
- };
-
- return (
-
-
-
- {phaseKey.toUpperCase()}
- {phase.duration && (
-
- ({phase.duration})
-
- )}
-
-
- {phase.deliverables && Array.isArray(phase.deliverables) && (
-
-
- Deliverables
-
-
- {phase.deliverables.map((item: string, idx: number) => (
-
-
- {item}
-
- ))}
-
-
- )}
-
- {phase.tasks && Array.isArray(phase.tasks) && (
-
-
- Tasks
-
-
- {phase.tasks.map((task: any, idx: number) => (
-
-
- {typeof task === 'string' ? task : task.description || JSON.stringify(task)}
-
- ))}
-
-
- )}
-
- {/* Render any other phase properties */}
- {Object.entries(phase).map(([key, value]) => {
- if (['duration', 'deliverables', 'tasks'].includes(key)) return null;
- return (
-
-
- {key.replace(/_/g, ' ')}
-
-
- {typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
-
-
- );
- })}
-
- );
- };
-
- // Check if this is a phased plan or a general plan structure
- const isPhased = Object.values(data).some(value =>
- typeof value === 'object' &&
- value !== null &&
- (value.duration || value.deliverables || value.tasks)
- );
-
- if (isPhased) {
- return (
-
- {Object.entries(data).map(([phaseKey, phase], index) =>
- renderPhase(phaseKey, phase as PRPPhase, index)
- )}
-
- );
- }
-
- // Fallback to generic rendering for non-phased plans
- return (
-
-
-
- {title}
-
-
- {Object.entries(data).map(([key, value]) => (
-
-
- {key.replace(/_/g, ' ').charAt(0).toUpperCase() + key.replace(/_/g, ' ').slice(1)}:
- {' '}
-
- {typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
-
-
- ))}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/RolloutPlanSection.tsx b/archon-ui-main/src/components/prp/sections/RolloutPlanSection.tsx
deleted file mode 100644
index cf3127c313..0000000000
--- a/archon-ui-main/src/components/prp/sections/RolloutPlanSection.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import React from 'react';
-import { Calendar, CheckCircle, AlertCircle } from 'lucide-react';
-import { PRPSectionProps } from '../types/prp.types';
-import { formatKey, formatValue } from '../utils/formatters';
-import { CollapsibleSectionWrapper } from '../components/CollapsibleSectionWrapper';
-
-/**
- * Component for rendering rollout plans and deployment strategies
- */
-export const RolloutPlanSection: React.FC = ({
- title,
- data,
- icon = ,
- accentColor = 'orange',
- isDarkMode = false,
- defaultOpen = true,
- isCollapsible = true,
- isOpen,
- onToggle
-}) => {
- if (!data) return null;
-
- const colorMap = {
- blue: 'from-blue-400 to-blue-600 border-blue-500',
- purple: 'from-purple-400 to-purple-600 border-purple-500',
- green: 'from-green-400 to-green-600 border-green-500',
- orange: 'from-orange-400 to-orange-600 border-orange-500',
- pink: 'from-pink-400 to-pink-600 border-pink-500',
- cyan: 'from-cyan-400 to-cyan-600 border-cyan-500',
- indigo: 'from-indigo-400 to-indigo-600 border-indigo-500',
- emerald: 'from-emerald-400 to-emerald-600 border-emerald-500',
- };
-
- const bgColorMap = {
- blue: 'bg-blue-50 dark:bg-blue-950',
- purple: 'bg-purple-50 dark:bg-purple-950',
- green: 'bg-green-50 dark:bg-green-950',
- orange: 'bg-orange-50 dark:bg-orange-950',
- pink: 'bg-pink-50 dark:bg-pink-950',
- cyan: 'bg-cyan-50 dark:bg-cyan-950',
- indigo: 'bg-indigo-50 dark:bg-indigo-950',
- emerald: 'bg-emerald-50 dark:bg-emerald-950',
- };
-
- const renderPhase = (phase: any, index: number) => {
- if (typeof phase === 'string') {
- return (
-
- );
- }
-
- if (typeof phase === 'object' && phase !== null) {
- const phaseName = phase.name || phase.title || phase.phase || `Phase ${index + 1}`;
- const duration = phase.duration || phase.timeline || phase.timeframe;
- const description = phase.description || phase.details || phase.summary;
- const tasks = phase.tasks || phase.activities || phase.items;
- const risks = phase.risks || phase.considerations;
-
- return (
-
-
-
- {index + 1}
-
-
-
{phaseName}
- {duration && (
-
{duration}
- )}
-
-
-
- {description && (
-
{description}
- )}
-
- {tasks && Array.isArray(tasks) && tasks.length > 0 && (
-
-
Tasks:
-
- {tasks.map((task, taskIndex) => (
-
-
- {formatValue(task)}
-
- ))}
-
-
- )}
-
- {risks && Array.isArray(risks) && risks.length > 0 && (
-
-
Risks & Considerations:
-
- {risks.map((risk, riskIndex) => (
-
-
- {formatValue(risk)}
-
- ))}
-
-
- )}
-
- {/* Render any other properties */}
- {Object.entries(phase).map(([key, value]) => {
- if (['name', 'title', 'phase', 'duration', 'timeline', 'timeframe', 'description', 'details', 'summary', 'tasks', 'activities', 'items', 'risks', 'considerations'].includes(key)) {
- return null;
- }
-
- return (
-
-
{formatKey(key)}:
-
- {typeof value === 'string' || typeof value === 'number' ? (
-
{value}
- ) : Array.isArray(value) ? (
-
- {value.map((item, i) => (
-
- •
- {formatValue(item)}
-
- ))}
-
- ) : (
-
- {JSON.stringify(value, null, 2)}
-
- )}
-
-
- );
- })}
-
- );
- }
-
- return null;
- };
-
- const renderRolloutPlan = () => {
- // Handle array of phases
- if (Array.isArray(data)) {
- return (
-
- {data.map((phase, index) => renderPhase(phase, index))}
-
- );
- }
-
- // Handle object with phases
- if (typeof data === 'object' && data !== null) {
- const phases = data.phases || data.plan || data.steps || data.stages;
-
- if (phases && Array.isArray(phases)) {
- return (
-
- {phases.map((phase, index) => renderPhase(phase, index))}
-
- );
- }
-
- // Handle object with other properties
- return (
-
- {Object.entries(data).map(([key, value]) => (
-
-
- {formatKey(key)}
-
- {Array.isArray(value) ? (
-
- {value.map((item, index) => renderPhase(item, index))}
-
- ) : typeof value === 'object' && value !== null ? (
-
- {renderPhase(value, 0)}
-
- ) : (
-
{formatValue(value)}
- )}
-
- ))}
-
- );
- }
-
- // Handle string
- if (typeof data === 'string') {
- return {data}
;
- }
-
- return null;
- };
-
- const header = (
-
-
-
- {icon}
-
-
- {title}
-
-
-
- );
-
- const content = (
-
- {renderRolloutPlan()}
-
- );
-
- return (
-
-
- {content}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/sections/TokenSystemSection.tsx b/archon-ui-main/src/components/prp/sections/TokenSystemSection.tsx
deleted file mode 100644
index e57c30f430..0000000000
--- a/archon-ui-main/src/components/prp/sections/TokenSystemSection.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import React from 'react';
-import { Palette, Layers } from 'lucide-react';
-import { PRPSectionProps } from '../types/prp.types';
-import { formatKey, formatValue } from '../utils/formatters';
-
-/**
- * Component for rendering design token systems and style guides
- */
-export const TokenSystemSection: React.FC = ({
- title,
- data,
- icon = ,
- accentColor = 'indigo',
- isDarkMode = false,
- defaultOpen = true
-}) => {
- if (!data) return null;
-
- const colorMap = {
- blue: 'from-blue-400 to-blue-600 border-blue-500',
- purple: 'from-purple-400 to-purple-600 border-purple-500',
- green: 'from-green-400 to-green-600 border-green-500',
- orange: 'from-orange-400 to-orange-600 border-orange-500',
- pink: 'from-pink-400 to-pink-600 border-pink-500',
- cyan: 'from-cyan-400 to-cyan-600 border-cyan-500',
- indigo: 'from-indigo-400 to-indigo-600 border-indigo-500',
- emerald: 'from-emerald-400 to-emerald-600 border-emerald-500',
- };
-
- const bgColorMap = {
- blue: 'bg-blue-50 dark:bg-blue-950',
- purple: 'bg-purple-50 dark:bg-purple-950',
- green: 'bg-green-50 dark:bg-green-950',
- orange: 'bg-orange-50 dark:bg-orange-950',
- pink: 'bg-pink-50 dark:bg-pink-950',
- cyan: 'bg-cyan-50 dark:bg-cyan-950',
- indigo: 'bg-indigo-50 dark:bg-indigo-950',
- emerald: 'bg-emerald-50 dark:bg-emerald-950',
- };
-
- const renderColorSwatch = (color: string, name: string) => {
- // Check if it's a valid color value
- const isHex = /^#[0-9A-F]{6}$/i.test(color);
- const isRgb = /^rgb/.test(color);
- const isHsl = /^hsl/.test(color);
- const isNamedColor = /^[a-z]+$/i.test(color);
-
- if (isHex || isRgb || isHsl || isNamedColor) {
- return (
-
- );
- }
-
- return null;
- };
-
- const renderSpacingValue = (value: string | number, name: string) => {
- const numValue = typeof value === 'string' ? parseFloat(value) : value;
- const unit = typeof value === 'string' ? value.replace(/[0-9.-]/g, '') : 'px';
-
- return (
-
-
-
-
{name}
-
{value}{unit}
-
-
- );
- };
-
- const renderTokenGroup = (tokens: any, groupName: string) => {
- if (!tokens || typeof tokens !== 'object') return null;
-
- const entries = Object.entries(tokens);
- const isColorGroup = groupName.toLowerCase().includes('color') ||
- entries.some(([_, v]) => typeof v === 'string' && (v.startsWith('#') || v.startsWith('rgb')));
- const isSpacingGroup = groupName.toLowerCase().includes('spacing') ||
- groupName.toLowerCase().includes('size') ||
- groupName.toLowerCase().includes('radius');
-
- return (
-
-
-
- {formatKey(groupName)}
-
-
- {entries.map(([key, value]) => {
- if (isColorGroup && typeof value === 'string') {
- const swatch = renderColorSwatch(value, formatKey(key));
- if (swatch) return
{swatch}
;
- }
-
- if (isSpacingGroup && (typeof value === 'string' || typeof value === 'number')) {
- return
{renderSpacingValue(value, formatKey(key))}
;
- }
-
- // Handle nested token groups
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
- return (
-
- {renderTokenGroup(value, key)}
-
- );
- }
-
- // Default rendering
- return (
-
- {formatKey(key)}:
- {formatValue(value)}
-
- );
- })}
-
-
- );
- };
-
- const renderTokenSystem = () => {
- // Handle string description
- if (typeof data === 'string') {
- return {data}
;
- }
-
- // Handle array of token groups
- if (Array.isArray(data)) {
- return (
-
- {data.map((group, index) => (
-
- {typeof group === 'object' && group !== null ? (
- renderTokenGroup(group, `Group ${index + 1}`)
- ) : (
-
{formatValue(group)}
- )}
-
- ))}
-
- );
- }
-
- // Handle object with token categories
- if (typeof data === 'object' && data !== null) {
- const categories = Object.entries(data);
-
- // Special handling for common token categories
- const colorTokens = categories.filter(([k]) => k.toLowerCase().includes('color'));
- const typographyTokens = categories.filter(([k]) => k.toLowerCase().includes('typography') || k.toLowerCase().includes('font'));
- const spacingTokens = categories.filter(([k]) => k.toLowerCase().includes('spacing') || k.toLowerCase().includes('size'));
- const otherTokens = categories.filter(([k]) =>
- !k.toLowerCase().includes('color') &&
- !k.toLowerCase().includes('typography') &&
- !k.toLowerCase().includes('font') &&
- !k.toLowerCase().includes('spacing') &&
- !k.toLowerCase().includes('size')
- );
-
- return (
-
- {/* Colors */}
- {colorTokens.length > 0 && (
-
- {colorTokens.map(([key, value]) => (
-
{renderTokenGroup(value, key)}
- ))}
-
- )}
-
- {/* Typography */}
- {typographyTokens.length > 0 && (
-
- {typographyTokens.map(([key, value]) => (
-
{renderTokenGroup(value, key)}
- ))}
-
- )}
-
- {/* Spacing */}
- {spacingTokens.length > 0 && (
-
- {spacingTokens.map(([key, value]) => (
-
{renderTokenGroup(value, key)}
- ))}
-
- )}
-
- {/* Others */}
- {otherTokens.length > 0 && (
-
- {otherTokens.map(([key, value]) => (
-
{renderTokenGroup(value, key)}
- ))}
-
- )}
-
- );
- }
-
- return null;
- };
-
- return (
-
-
-
-
- {icon}
-
-
- {title}
-
-
-
- {renderTokenSystem()}
-
-
- );
-};
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/types/prp.types.ts b/archon-ui-main/src/components/prp/types/prp.types.ts
deleted file mode 100644
index ad9775c513..0000000000
--- a/archon-ui-main/src/components/prp/types/prp.types.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { ReactNode } from 'react';
-
-// Base section types
-export type SectionType =
- | 'metadata'
- | 'context'
- | 'personas'
- | 'flows'
- | 'metrics'
- | 'plan'
- | 'list'
- | 'object'
- | 'keyvalue'
- | 'features'
- | 'generic';
-
-export interface SectionProps {
- title: string;
- data: any;
- icon?: ReactNode;
- accentColor?: string;
- defaultOpen?: boolean;
- isDarkMode?: boolean;
- isCollapsible?: boolean;
- onToggle?: () => void;
- isOpen?: boolean;
-}
-
-// Alias for component compatibility
-export type PRPSectionProps = SectionProps;
-
-export interface PRPMetadata {
- title?: string;
- version?: string;
- author?: string;
- date?: string;
- status?: string;
- document_type?: string;
- [key: string]: any;
-}
-
-export interface PRPContext {
- scope?: string;
- background?: string;
- objectives?: string[];
- requirements?: any;
- [key: string]: any;
-}
-
-export interface PRPPersona {
- name?: string;
- role?: string;
- goals?: string[];
- pain_points?: string[];
- journey?: Record;
- workflow?: Record;
- [key: string]: any;
-}
-
-export interface PRPPhase {
- duration?: string;
- deliverables?: string[];
- tasks?: any[];
- [key: string]: any;
-}
-
-export interface PRPContent {
- // Common fields
- title?: string;
- version?: string;
- author?: string;
- date?: string;
- status?: string;
- document_type?: string;
-
- // Section fields
- context?: PRPContext;
- user_personas?: Record;
- user_flows?: Record;
- success_metrics?: Record>;
- implementation_plan?: Record;
- validation_gates?: Record;
- technical_implementation?: Record;
- ui_improvements?: Record;
- information_architecture?: Record;
- current_state_analysis?: Record;
- component_architecture?: Record;
-
- // Allow any other fields
- [key: string]: any;
-}
-
-export interface SectionDetectorResult {
- type: SectionType;
- confidence: number;
-}
-
-export interface SectionComponentProps extends SectionProps {
- content: PRPContent;
- sectionKey: string;
-}
-
-// Color maps for consistent theming
-export const sectionColorMap: Record = {
- metadata: 'blue',
- context: 'purple',
- personas: 'pink',
- flows: 'orange',
- metrics: 'green',
- plan: 'cyan',
- technical: 'indigo',
- validation: 'emerald',
- generic: 'gray'
-};
-
-// Icon size constants
-export const ICON_SIZES = {
- section: 'w-5 h-5',
- subsection: 'w-4 h-4',
- item: 'w-3 h-3'
-} as const;
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/utils/formatters.ts b/archon-ui-main/src/components/prp/utils/formatters.ts
deleted file mode 100644
index a71108b315..0000000000
--- a/archon-ui-main/src/components/prp/utils/formatters.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { normalizeImagePlaceholders } from './normalizer';
-
-/**
- * Formats a key into a human-readable label
- */
-export function formatKey(key: string): string {
- return key
- .replace(/_/g, ' ')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .split(' ')
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
-}
-
-/**
- * Truncates text with ellipsis
- */
-export function truncateText(text: string, maxLength: number = 100): string {
- if (text.length <= maxLength) return text;
- return text.substring(0, maxLength - 3) + '...';
-}
-
-/**
- * Formats a value for display
- */
-export function formatValue(value: any): string {
- if (value === null || value === undefined) return '';
- if (typeof value === 'boolean') return value ? 'Yes' : 'No';
- if (typeof value === 'number') return value.toLocaleString();
- if (typeof value === 'string') {
- // Temporarily disabled to debug black screen issue
- // return normalizeImagePlaceholders(value);
- return value;
- }
- if (Array.isArray(value)) return `${value.length} items`;
- if (typeof value === 'object') return `${Object.keys(value).length} properties`;
- return String(value);
-}
-
-/**
- * Gets accent color based on index for variety
- */
-export function getAccentColor(index: number): string {
- const colors = ['blue', 'purple', 'green', 'orange', 'pink', 'cyan', 'indigo', 'emerald'];
- return colors[index % colors.length];
-}
-
-/**
- * Generates a unique key for React components
- */
-export function generateKey(prefix: string, ...parts: (string | number)[]): string {
- return [prefix, ...parts].filter(Boolean).join('-');
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/utils/markdownParser.ts b/archon-ui-main/src/components/prp/utils/markdownParser.ts
deleted file mode 100644
index aefa0afb74..0000000000
--- a/archon-ui-main/src/components/prp/utils/markdownParser.ts
+++ /dev/null
@@ -1,397 +0,0 @@
-/**
- * Markdown Parser for PRP Documents
- *
- * Parses raw markdown content into structured sections that can be rendered
- * by the PRPViewer component with collapsible sections and beautiful formatting.
- */
-
-export interface ParsedSection {
- title: string;
- content: string;
- level: number;
- type: 'text' | 'list' | 'code' | 'mixed';
- rawContent: string;
- sectionKey: string;
- templateType?: string; // For matching to PRP templates
-}
-
-export interface ParsedMarkdownDocument {
- title?: string;
- sections: ParsedSection[];
- metadata: Record;
- hasMetadata: boolean;
-}
-
-export interface ParsedMarkdown {
- title?: string;
- sections: Record;
- metadata: Record;
-}
-
-/**
- * Parses markdown content into structured sections based on headers
- */
-export function parseMarkdownToPRP(content: string): ParsedMarkdown {
- if (!content || typeof content !== 'string') {
- return { sections: {}, metadata: {} };
- }
-
- const lines = content.split('\n');
- const sections: Record = {};
- let currentSection: ParsedSection | null = null;
- let documentTitle: string | undefined;
- let sectionCounter = 0;
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
-
- // Check for headers (## Section Name or # Document Title)
- const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
-
- if (headerMatch) {
- const level = headerMatch[1].length;
- const title = headerMatch[2].trim();
-
- // Save previous section if exists
- if (currentSection) {
- const sectionKey = generateSectionKey(currentSection.title, sectionCounter);
- sections[sectionKey] = {
- ...currentSection,
- content: currentSection.content.trim(),
- rawContent: currentSection.rawContent.trim(),
- type: detectContentType(currentSection.content)
- };
- sectionCounter++;
- }
-
- // Handle document title (# level headers)
- if (level === 1 && !documentTitle) {
- documentTitle = title;
- currentSection = null;
- continue;
- }
-
- // Start new section
- currentSection = {
- title,
- content: '',
- level,
- type: 'text',
- rawContent: ''
- };
- } else if (currentSection) {
- // Add content to current section
- currentSection.content += line + '\n';
- currentSection.rawContent += line + '\n';
- } else if (!documentTitle && line.trim()) {
- // If we haven't found a title yet and encounter content, treat first non-empty line as title
- documentTitle = line.trim();
- }
- }
-
- // Save final section
- if (currentSection) {
- const sectionKey = generateSectionKey(currentSection.title, sectionCounter);
- sections[sectionKey] = {
- ...currentSection,
- content: currentSection.content.trim(),
- rawContent: currentSection.rawContent.trim(),
- type: detectContentType(currentSection.content)
- };
- }
-
- return {
- title: documentTitle,
- sections,
- metadata: {
- document_type: 'prp',
- parsed_from_markdown: true,
- section_count: Object.keys(sections).length
- }
- };
-}
-
-/**
- * Generates a consistent section key for use in the sections object
- */
-function generateSectionKey(title: string, counter: number): string {
- // Convert title to a key format
- const baseKey = title
- .toLowerCase()
- .replace(/[^a-z0-9\s]/g, '')
- .replace(/\s+/g, '_')
- .substring(0, 30); // Limit length
-
- return baseKey || `section_${counter}`;
-}
-
-/**
- * Detects the type of content in a section
- */
-function detectContentType(content: string): 'text' | 'list' | 'code' | 'mixed' {
- if (!content.trim()) return 'text';
-
- const lines = content.split('\n').filter(line => line.trim());
- let hasText = false;
- let hasList = false;
- let hasCode = false;
-
- for (const line of lines) {
- if (line.startsWith('```')) {
- hasCode = true;
- } else if (line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/)) {
- hasList = true;
- } else if (line.trim()) {
- hasText = true;
- }
- }
-
- if (hasCode) return 'code';
- if (hasList && hasText) return 'mixed';
- if (hasList) return 'list';
- return 'text';
-}
-
-/**
- * Converts parsed markdown back to a structure compatible with PRPViewer
- * Each section becomes a separate collapsible section in the viewer
- */
-export function convertParsedMarkdownToPRPStructure(parsed: ParsedMarkdown): any {
- const result: any = {
- title: parsed.title || 'Untitled Document',
- ...parsed.metadata
- };
-
- // Add each section as a top-level property
- // The content will be the raw markdown for that section only
- for (const [key, section] of Object.entries(parsed.sections)) {
- result[key] = section.rawContent;
- }
-
- return result;
-}
-
-/**
- * Checks if content appears to be raw markdown
- */
-export function isMarkdownContent(content: any): boolean {
- if (typeof content !== 'string') return false;
-
- // Look for markdown indicators
- const markdownIndicators = [
- /^#{1,6}\s+.+$/m, // Headers
- /^[-*+]\s+.+$/m, // Bullet lists
- /^\d+\.\s+.+$/m, // Numbered lists
- /```/, // Code blocks
- /^\>.+$/m, // Blockquotes
- /\*\*.+\*\*/, // Bold text
- /\*.+\*/, // Italic text
- ];
-
- return markdownIndicators.some(pattern => pattern.test(content));
-}
-
-/**
- * Parses markdown content into a flowing document structure
- */
-export function parseMarkdownToDocument(content: string): ParsedMarkdownDocument {
- if (!content || typeof content !== 'string') {
- return { sections: [], metadata: {}, hasMetadata: false };
- }
-
- const lines = content.split('\n');
- const sections: ParsedSection[] = [];
- let currentSection: Partial | null = null;
- let documentTitle: string | undefined;
- let sectionCounter = 0;
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
-
- // Check for headers (## Section Name or # Document Title)
- const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
-
- if (headerMatch) {
- const level = headerMatch[1].length;
- const title = headerMatch[2].trim();
-
- // Save previous section if exists
- if (currentSection && currentSection.title) {
- sections.push({
- title: currentSection.title,
- content: (currentSection.content || '').trim(),
- level: currentSection.level || 2,
- type: detectContentType(currentSection.content || ''),
- rawContent: (currentSection.rawContent || '').trim(),
- sectionKey: generateSectionKey(currentSection.title, sectionCounter),
- templateType: detectTemplateType(currentSection.title)
- });
- sectionCounter++;
- }
-
- // Handle document title (# level headers)
- if (level === 1 && !documentTitle) {
- documentTitle = title;
- currentSection = null;
- continue;
- }
-
- // Start new section
- currentSection = {
- title,
- content: '',
- level,
- rawContent: ''
- };
- } else if (currentSection) {
- // Add content to current section
- currentSection.content = (currentSection.content || '') + line + '\n';
- currentSection.rawContent = (currentSection.rawContent || '') + line + '\n';
- } else if (!documentTitle && line.trim()) {
- // If we haven't found a title yet and encounter content, treat first non-empty line as title
- documentTitle = line.trim();
- }
- }
-
- // Save final section
- if (currentSection && currentSection.title) {
- sections.push({
- title: currentSection.title,
- content: (currentSection.content || '').trim(),
- level: currentSection.level || 2,
- type: detectContentType(currentSection.content || ''),
- rawContent: (currentSection.rawContent || '').trim(),
- sectionKey: generateSectionKey(currentSection.title, sectionCounter),
- templateType: detectTemplateType(currentSection.title)
- });
- }
-
- return {
- title: documentTitle,
- sections,
- metadata: {
- document_type: 'prp', // Set as PRP to get the right styling
- section_count: sections.length,
- parsed_from_markdown: true
- },
- hasMetadata: false
- };
-}
-
-/**
- * Detects if a section title matches a known PRP template type
- */
-function detectTemplateType(title: string): string | undefined {
- const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
-
- // Map common PRP section names to template types
- const templateMap: Record = {
- 'goal': 'context',
- 'objective': 'context',
- 'purpose': 'context',
- 'why': 'context',
- 'rationale': 'context',
- 'what': 'context',
- 'description': 'context',
- 'overview': 'context',
- 'context': 'context',
- 'background': 'context',
- 'problem statement': 'context',
-
- 'success metrics': 'metrics',
- 'metrics': 'metrics',
- 'kpis': 'metrics',
- 'success criteria': 'metrics',
- 'estimated impact': 'metrics',
-
- 'implementation plan': 'plan',
- 'plan': 'plan',
- 'roadmap': 'plan',
- 'timeline': 'plan',
- 'phases': 'plan',
- 'rollout plan': 'plan',
- 'migration strategy': 'plan',
-
- 'personas': 'personas',
- 'users': 'personas',
- 'stakeholders': 'personas',
- 'target audience': 'personas',
-
- 'user flow': 'flows',
- 'user journey': 'flows',
- 'workflow': 'flows',
- 'user experience': 'flows',
-
- 'validation': 'list',
- 'testing': 'list',
- 'quality gates': 'list',
- 'acceptance criteria': 'list',
-
- 'features': 'features',
- 'feature requirements': 'features',
- 'capabilities': 'features',
-
- 'technical requirements': 'object',
- 'architecture': 'object',
- 'design': 'object',
- 'components': 'object',
-
- 'budget': 'keyvalue',
- 'resources': 'keyvalue',
- 'team': 'keyvalue',
- 'cost': 'keyvalue'
- };
-
- return templateMap[normalizedTitle];
-}
-
-/**
- * Checks if content is a document with metadata structure
- */
-export function isDocumentWithMetadata(content: any): boolean {
- if (typeof content !== 'object' || content === null) return false;
-
- // Check if it has typical document metadata fields
- const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
- const hasMetadata = metadataFields.some(field => field in content);
-
- // Check if it has a content field that looks like markdown
- const hasMarkdownContent = typeof content.content === 'string' &&
- isMarkdownContent(content.content);
-
- // Also check if any field contains markdown content (broader detection)
- const hasAnyMarkdownField = Object.values(content).some(value =>
- typeof value === 'string' && isMarkdownContent(value)
- );
-
- // Return true if it has metadata AND markdown content, OR if it has obvious document structure
- return (hasMetadata && (hasMarkdownContent || hasAnyMarkdownField)) ||
- (hasMetadata && Object.keys(content).length <= 10); // Simple document structure
-}
-
-/**
- * Main function to process content for PRPViewer
- */
-export function processContentForPRP(content: any): any {
- // If it's already an object, return as-is
- if (typeof content === 'object' && content !== null) {
- return content;
- }
-
- // If it's a string that looks like markdown, parse it
- if (typeof content === 'string' && isMarkdownContent(content)) {
- const parsed = parseMarkdownToPRP(content);
- return convertParsedMarkdownToPRPStructure(parsed);
- }
-
- // For any other string content, wrap it in a generic structure
- if (typeof content === 'string') {
- return {
- title: 'Document Content',
- content: content,
- document_type: 'text'
- };
- }
-
- return content;
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/utils/normalizer.ts b/archon-ui-main/src/components/prp/utils/normalizer.ts
deleted file mode 100644
index 074c0d39b8..0000000000
--- a/archon-ui-main/src/components/prp/utils/normalizer.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * Normalizes PRP document data to ensure consistent rendering
- */
-
-/**
- * Normalizes image placeholders to proper markdown format
- */
-export function normalizeImagePlaceholders(content: string): string {
- return content.replace(/\[Image #(\d+)\]/g, (match, num) => {
- return ``;
- });
-}
-
-/**
- * Attempts to parse JSON strings into objects
- */
-export function parseJsonStrings(value: any): any {
- if (typeof value === 'string') {
- const trimmed = value.trim();
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
- (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
- try {
- return JSON.parse(trimmed);
- } catch (e) {
- // Return original string if parsing fails
- return value;
- }
- }
-
- // Normalize image placeholders in strings
- return normalizeImagePlaceholders(value);
- }
-
- if (Array.isArray(value)) {
- return value.map(item => parseJsonStrings(item));
- }
-
- if (value && typeof value === 'object') {
- const normalized: any = {};
- for (const [key, val] of Object.entries(value)) {
- normalized[key] = parseJsonStrings(val);
- }
- return normalized;
- }
-
- return value;
-}
-
-/**
- * Flattens nested content fields
- */
-export function flattenNestedContent(data: any): any {
- // Handle nested content field
- if (data && typeof data === 'object' && 'content' in data) {
- const { content, ...rest } = data;
-
- // If content is an object, merge it with the rest
- if (content && typeof content === 'object' && !Array.isArray(content)) {
- return flattenNestedContent({ ...rest, ...content });
- }
-
- // If content is a string or array, keep it as a field
- return { ...rest, content };
- }
-
- return data;
-}
-
-/**
- * Normalizes section names to be more readable
- */
-export function normalizeSectionName(name: string): string {
- // Common abbreviations and their expansions
- const expansions: Record = {
- 'ui': 'User Interface',
- 'ux': 'User Experience',
- 'api': 'API',
- 'kpi': 'KPI',
- 'prp': 'PRP',
- 'prd': 'PRD',
- 'mvp': 'MVP',
- 'poc': 'Proof of Concept',
- };
-
- // Split by underscore or camelCase
- const words = name
- .replace(/_/g, ' ')
- .replace(/([a-z])([A-Z])/g, '$1 $2')
- .split(' ')
- .filter(word => word.length > 0);
-
- // Process each word
- const processed = words.map(word => {
- const lower = word.toLowerCase();
-
- // Check if it's a known abbreviation
- if (expansions[lower]) {
- return expansions[lower];
- }
-
- // Otherwise, capitalize first letter
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
- });
-
- return processed.join(' ');
-}
-
-/**
- * Normalizes the entire PRP document structure
- */
-export function normalizePRPDocument(content: any): any {
- if (!content) return content;
-
- // First, flatten any nested content fields
- let normalized = flattenNestedContent(content);
-
- // Then parse any JSON strings
- normalized = parseJsonStrings(normalized);
-
- // Handle raw markdown content
- if (typeof normalized === 'string') {
- // For strings, just normalize image placeholders and return as-is
- // The PRPViewer will handle the markdown parsing
- return normalizeImagePlaceholders(normalized);
- }
-
- // For objects, process each field recursively
- if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) {
- const result: any = {};
-
- for (const [key, value] of Object.entries(normalized)) {
- // Skip empty values
- if (value === null || value === undefined ||
- (typeof value === 'string' && value.trim() === '') ||
- (Array.isArray(value) && value.length === 0) ||
- (typeof value === 'object' && Object.keys(value).length === 0)) {
- continue;
- }
-
- // Recursively process nested values
- if (typeof value === 'string') {
- result[key] = normalizeImagePlaceholders(value);
- } else if (Array.isArray(value)) {
- result[key] = value.map(item =>
- typeof item === 'string' ? normalizeImagePlaceholders(item) : normalizePRPDocument(item)
- );
- } else if (typeof value === 'object') {
- result[key] = normalizePRPDocument(value);
- } else {
- result[key] = value;
- }
- }
-
- return result;
- }
-
- // For arrays, process each item
- if (Array.isArray(normalized)) {
- return normalized.map(item =>
- typeof item === 'string' ? normalizeImagePlaceholders(item) : normalizePRPDocument(item)
- );
- }
-
- return normalized;
-}
-
-/**
- * Checks if a value contains complex nested structures
- */
-export function hasComplexNesting(value: any): boolean {
- if (!value || typeof value !== 'object') return false;
-
- if (Array.isArray(value)) {
- return value.some(item =>
- typeof item === 'object' && item !== null
- );
- }
-
- return Object.values(value).some(val =>
- (typeof val === 'object' && val !== null) ||
- (Array.isArray(val) && val.some(item => typeof item === 'object'))
- );
-}
-
-/**
- * Extracts metadata fields from content
- */
-export function extractMetadata(content: any): { metadata: any; sections: any } {
- if (!content || typeof content !== 'object') {
- return { metadata: {}, sections: content };
- }
-
- const metadataFields = [
- 'title', 'version', 'author', 'date', 'status',
- 'document_type', 'created_at', 'updated_at',
- 'id', '_id', 'project_id'
- ];
-
- const metadata: any = {};
- const sections: any = {};
-
- for (const [key, value] of Object.entries(content)) {
- if (metadataFields.includes(key)) {
- metadata[key] = value;
- } else {
- sections[key] = value;
- }
- }
-
- return { metadata, sections };
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/utils/objectRenderer.tsx b/archon-ui-main/src/components/prp/utils/objectRenderer.tsx
deleted file mode 100644
index 9b01db2643..0000000000
--- a/archon-ui-main/src/components/prp/utils/objectRenderer.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import { formatKey, formatValue } from './formatters';
-
-/**
- * Renders any value in a formatted way without using JSON.stringify
- */
-export function renderValue(value: any, depth: number = 0): React.ReactNode {
- try {
- // Prevent infinite recursion
- if (depth > 10) {
- return Too deeply nested ;
- }
-
- // Handle null/undefined
- if (value === null || value === undefined) {
- return Empty ;
- }
-
- // Handle primitives
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
- return {formatValue(value)} ;
- }
-
- // Handle arrays
- if (Array.isArray(value)) {
- if (value.length === 0) {
- return No items ;
- }
-
- // Check if it's a simple array
- const isSimple = value.every(item =>
- typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
- );
-
- if (isSimple) {
- return (
-
- {value.map((item, index) => (
-
- {formatValue(item)}
-
- ))}
-
- );
- }
-
- // Complex array
- return (
-
- {value.map((item, index) => (
-
-
Item {index + 1}
- {renderValue(item, depth + 1)}
-
- ))}
-
- );
- }
-
- // Handle objects
- if (typeof value === 'object' && value !== null) {
- const entries = Object.entries(value);
- if (entries.length === 0) {
- return No properties ;
- }
-
- return (
-
- {entries.map(([key, val]) => (
-
-
- {formatKey(key)}:
-
-
- {renderValue(val, depth + 1)}
-
-
- ))}
-
- );
- }
-
- // Fallback
- return {String(value)} ;
- } catch (error) {
- console.error('Error rendering value:', error, value);
- return Error rendering content ;
- }
-}
-
-/**
- * Renders a value inline for simple display
- */
-export function renderValueInline(value: any): string {
- if (value === null || value === undefined) return '';
- if (typeof value === 'string') return formatValue(value);
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
- if (Array.isArray(value)) return value.map(v => renderValueInline(v)).join(', ');
- if (typeof value === 'object') {
- // For objects, just show a summary
- const keys = Object.keys(value);
- if (keys.length === 0) return 'Empty object';
- if (keys.length <= 3) return keys.map(k => `${k}: ${renderValueInline(value[k])}`).join(', ');
- return `${keys.length} properties`;
- }
- return String(value);
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/components/prp/utils/sectionDetector.ts b/archon-ui-main/src/components/prp/utils/sectionDetector.ts
deleted file mode 100644
index edcbcb97bb..0000000000
--- a/archon-ui-main/src/components/prp/utils/sectionDetector.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-import { SectionType, SectionDetectorResult } from '../types/prp.types';
-
-/**
- * Detects the type of a section based on its key and content structure
- */
-export function detectSectionType(key: string, value: any): SectionDetectorResult {
- const normalizedKey = key.toLowerCase().replace(/_/g, '').replace(/\s+/g, '');
-
- // Check metadata fields
- if (['title', 'version', 'author', 'date', 'status', 'documenttype'].includes(normalizedKey)) {
- return { type: 'metadata', confidence: 1.0 };
- }
-
- // Check context sections (including common markdown headers)
- if (normalizedKey === 'context' || normalizedKey === 'overview' ||
- normalizedKey === 'executivesummary' || normalizedKey === 'problemstatement' ||
- normalizedKey === 'visionstatement' || normalizedKey === 'proposedsolution' ||
- normalizedKey === 'goal' || normalizedKey === 'objective' || normalizedKey === 'purpose' ||
- normalizedKey === 'why' || normalizedKey === 'rationale' || normalizedKey === 'what' ||
- normalizedKey === 'description' || normalizedKey === 'background') {
- return { type: 'context', confidence: 1.0 };
- }
-
- // Check personas
- if (normalizedKey.includes('persona') || normalizedKey.includes('user') ||
- normalizedKey === 'stakeholders' || normalizedKey === 'targetaudience') {
- // Always treat these as personas, even if structure doesn't match perfectly
- return { type: 'personas', confidence: 0.9 };
- }
-
- // Check flows/journeys
- if (normalizedKey.includes('flow') || normalizedKey.includes('journey') ||
- normalizedKey.includes('workflow') || normalizedKey === 'userexperience') {
- return { type: 'flows', confidence: 0.9 };
- }
-
- // Check metrics (including common markdown headers)
- if (normalizedKey.includes('metric') || normalizedKey.includes('success') ||
- normalizedKey.includes('kpi') || normalizedKey === 'estimatedimpact' ||
- normalizedKey === 'successmetrics' || normalizedKey === 'successcriteria') {
- return { type: 'metrics', confidence: 0.9 };
- }
-
- // Check implementation plans (including common markdown headers)
- if (normalizedKey.includes('plan') || normalizedKey.includes('phase') ||
- normalizedKey.includes('implementation') || normalizedKey.includes('roadmap') ||
- normalizedKey === 'timeline' || normalizedKey === 'rolloutplan' ||
- normalizedKey === 'migrationstrategy' || normalizedKey === 'implementationplan') {
- return { type: 'plan', confidence: 0.9 };
- }
-
- // Check validation/testing (including common markdown headers)
- if (normalizedKey.includes('validation') || normalizedKey.includes('test') ||
- normalizedKey.includes('gate') || normalizedKey === 'compliance' ||
- normalizedKey.includes('quality') || normalizedKey === 'accessibilitystandards' ||
- normalizedKey === 'acceptancecriteria' || normalizedKey === 'qualitygates') {
- return { type: 'list', confidence: 0.8 };
- }
-
- // Check risk assessment
- if (normalizedKey.includes('risk') || normalizedKey === 'riskassessment') {
- return { type: 'list', confidence: 0.9 };
- }
-
- // Check design/architecture sections
- if (normalizedKey.includes('design') || normalizedKey.includes('architecture') ||
- normalizedKey.includes('component') || normalizedKey === 'tokensystem' ||
- normalizedKey === 'designprinciples' || normalizedKey === 'designguidelines') {
- return { type: 'object', confidence: 0.8 };
- }
-
- // Check budget/resources
- if (normalizedKey.includes('budget') || normalizedKey.includes('resource') ||
- normalizedKey.includes('cost') || normalizedKey === 'team' ||
- normalizedKey === 'budgetestimate' || normalizedKey === 'budgetandresources') {
- return { type: 'keyvalue', confidence: 0.9 };
- }
-
- // Check feature requirements specifically
- if (normalizedKey === 'featurerequirements' || normalizedKey === 'features' ||
- normalizedKey === 'capabilities') {
- return { type: 'features', confidence: 0.9 };
- }
-
- // Check requirements
- if (normalizedKey.includes('requirement') ||
- normalizedKey === 'technicalrequirements') {
- return { type: 'object', confidence: 0.8 };
- }
-
- // Check data/information sections
- if (normalizedKey.includes('data') || normalizedKey.includes('information') ||
- normalizedKey === 'currentstateanalysis' || normalizedKey === 'informationarchitecture') {
- return { type: 'object', confidence: 0.8 };
- }
-
- // Check governance/process sections
- if (normalizedKey.includes('governance') || normalizedKey.includes('process') ||
- normalizedKey === 'governancemodel' || normalizedKey === 'testingstrategy') {
- return { type: 'object', confidence: 0.8 };
- }
-
- // Check technical sections
- if (normalizedKey.includes('technical') || normalizedKey.includes('tech') ||
- normalizedKey === 'aimodelspecifications' || normalizedKey === 'performancerequirements' ||
- normalizedKey === 'toolingandinfrastructure' || normalizedKey === 'monitoringandanalytics') {
- return { type: 'object', confidence: 0.8 };
- }
-
- // Analyze value structure
- if (Array.isArray(value)) {
- return { type: 'list', confidence: 0.7 };
- }
-
- if (typeof value === 'object' && value !== null) {
- // Check if it's a simple key-value object
- if (isSimpleKeyValue(value)) {
- return { type: 'keyvalue', confidence: 0.7 };
- }
-
- // Check if it's a complex nested object
- if (hasNestedObjects(value)) {
- return { type: 'object', confidence: 0.7 };
- }
- }
-
- // Default fallback
- return { type: 'generic', confidence: 0.5 };
-}
-
-/**
- * Checks if the value structure matches a persona pattern
- */
-function isPersonaStructure(value: any): boolean {
- if (typeof value !== 'object' || value === null) return false;
-
- // Check if it's a collection of personas
- const values = Object.values(value);
- if (values.length === 0) return false;
-
- // Check if first value has persona-like properties
- const firstValue = values[0];
- if (typeof firstValue !== 'object') return false;
-
- const personaKeys = ['name', 'role', 'goals', 'pain_points', 'journey', 'workflow'];
- return personaKeys.some(key => key in firstValue);
-}
-
-/**
- * Checks if an object is a simple key-value structure
- */
-function isSimpleKeyValue(obj: any): boolean {
- if (typeof obj !== 'object' || obj === null) return false;
-
- const values = Object.values(obj);
- return values.every(val =>
- typeof val === 'string' ||
- typeof val === 'number' ||
- typeof val === 'boolean'
- );
-}
-
-/**
- * Checks if an object has nested objects
- */
-function hasNestedObjects(obj: any): boolean {
- if (typeof obj !== 'object' || obj === null) return false;
-
- const values = Object.values(obj);
- return values.some(val =>
- typeof val === 'object' &&
- val !== null &&
- !Array.isArray(val)
- );
-}
-
-/**
- * Gets a suggested icon based on section key
- */
-export function getSectionIcon(key: string): string {
- const normalizedKey = key.toLowerCase();
-
- if (normalizedKey.includes('persona') || normalizedKey.includes('user')) return 'Users';
- if (normalizedKey.includes('flow') || normalizedKey.includes('journey')) return 'Workflow';
- if (normalizedKey.includes('metric') || normalizedKey.includes('success')) return 'BarChart3';
- if (normalizedKey.includes('plan') || normalizedKey.includes('implementation')) return 'Clock';
- if (normalizedKey.includes('context') || normalizedKey.includes('overview')) return 'Brain';
- if (normalizedKey.includes('technical') || normalizedKey.includes('tech')) return 'Code';
- if (normalizedKey.includes('validation') || normalizedKey.includes('test')) return 'Shield';
- if (normalizedKey.includes('component') || normalizedKey.includes('architecture')) return 'Layers';
-
- return 'FileText';
-}
-
-/**
- * Formats a section key into a human-readable title
- */
-export function formatSectionTitle(key: string): string {
- return key
- .replace(/_/g, ' ')
- .split(' ')
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
-}
\ No newline at end of file
diff --git a/archon-ui-main/src/contexts/ToastContext.tsx b/archon-ui-main/src/contexts/ToastContext.tsx
index e9769c171f..2e9e5c2143 100644
--- a/archon-ui-main/src/contexts/ToastContext.tsx
+++ b/archon-ui-main/src/contexts/ToastContext.tsx
@@ -27,7 +27,8 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const [toasts, setToasts] = useState([]);
const showToast = useCallback((message: string, type: Toast['type'] = 'info', duration = 4000) => {
- const id = Date.now().toString();
+ // Use timestamp + random number to prevent duplicate keys
+ const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const newToast: Toast = { id, message, type, duration };
setToasts(prev => [...prev, newToast]);
diff --git a/archon-ui-main/src/features/projects/components/NewProjectModal.tsx b/archon-ui-main/src/features/projects/components/NewProjectModal.tsx
new file mode 100644
index 0000000000..ed7da3e248
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/NewProjectModal.tsx
@@ -0,0 +1,145 @@
+import { Loader2 } from "lucide-react";
+import type React from "react";
+import { useId, useState } from "react";
+import { Button } from "../../ui/primitives/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "../../ui/primitives/dialog";
+import { Input } from "../../ui/primitives/input";
+import { cn } from "../../ui/primitives/styles";
+import { useCreateProject } from "../hooks/useProjectQueries";
+import type { CreateProjectRequest } from "../types";
+
+interface NewProjectModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSuccess?: () => void;
+}
+
+export const NewProjectModal: React.FC = ({ open, onOpenChange, onSuccess }) => {
+ const projectNameId = useId();
+ const projectDescriptionId = useId();
+
+ const [formData, setFormData] = useState({
+ title: "",
+ description: "",
+ });
+
+ const createProjectMutation = useCreateProject();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!formData.title.trim()) return;
+
+ createProjectMutation.mutate(formData, {
+ onSuccess: () => {
+ setFormData({ title: "", description: "" });
+ onOpenChange(false);
+ onSuccess?.();
+ },
+ });
+ };
+
+ const handleClose = () => {
+ if (!createProjectMutation.isPending) {
+ setFormData({ title: "", description: "" });
+ onOpenChange(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/components/ProjectCard.tsx b/archon-ui-main/src/features/projects/components/ProjectCard.tsx
new file mode 100644
index 0000000000..3417a8c11f
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/ProjectCard.tsx
@@ -0,0 +1,258 @@
+import { motion } from "framer-motion";
+import { Activity, CheckCircle2, ListTodo } from "lucide-react";
+import type React from "react";
+import { cn } from "../../ui/primitives/styles";
+import type { Project } from "../types";
+import { ProjectCardActions } from "./ProjectCardActions";
+
+interface ProjectCardProps {
+ project: Project;
+ isSelected: boolean;
+ taskCounts: {
+ todo: number;
+ doing: number;
+ review: number;
+ done: number;
+ };
+ onSelect: (project: Project) => void;
+ onPin: (e: React.MouseEvent, projectId: string) => void;
+ onDelete: (e: React.MouseEvent, projectId: string, title: string) => void;
+}
+
+export const ProjectCard: React.FC = ({
+ project,
+ isSelected,
+ taskCounts,
+ onSelect,
+ onPin,
+ onDelete,
+}) => {
+ return (
+ onSelect(project)}
+ className={cn(
+ "relative rounded-xl backdrop-blur-md w-72 min-h-[180px] cursor-pointer overflow-visible group flex flex-col",
+ "transition-all duration-300",
+ project.pinned
+ ? "bg-gradient-to-b from-purple-100/80 via-purple-50/30 to-purple-100/50 dark:from-purple-900/30 dark:via-purple-900/20 dark:to-purple-900/10"
+ : isSelected
+ ? "bg-gradient-to-b from-white/70 via-purple-50/20 to-white/50 dark:from-white/5 dark:via-purple-900/5 dark:to-black/20"
+ : "bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30",
+ "border",
+ project.pinned
+ ? "border-purple-500/80 dark:border-purple-500/80 shadow-[0_0_15px_rgba(168,85,247,0.3)]"
+ : isSelected
+ ? "border-purple-400/60 dark:border-purple-500/60"
+ : "border-gray-200 dark:border-zinc-800/50",
+ isSelected
+ ? "shadow-[0_0_15px_rgba(168,85,247,0.4),0_0_10px_rgba(147,51,234,0.3)] dark:shadow-[0_0_20px_rgba(168,85,247,0.5),0_0_15px_rgba(147,51,234,0.4)]"
+ : "shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]",
+ "hover:shadow-[0_15px_40px_-15px_rgba(0,0,0,0.2)] dark:hover:shadow-[0_15px_40px_-15px_rgba(0,0,0,0.9)]",
+ isSelected ? "scale-[1.02]" : "hover:scale-[1.01]", // Use scale instead of translate to avoid clipping
+ )}
+ >
+ {/* Subtle aurora glow effect for selected card */}
+ {isSelected && (
+
+ )}
+
+ {/* Main content area with padding */}
+
+ {/* Title section */}
+
+
+ {project.title}
+
+
+
+ {/* Task count pills */}
+
+ {/* Todo pill */}
+
+
+
+
+
+
+ ToDo
+
+
+
+
+ {taskCounts.todo || 0}
+
+
+
+
+
+ {/* Doing pill (includes review) */}
+
+
+
+
+
+
+ {(taskCounts.doing || 0) + (taskCounts.review || 0)}
+
+
+
+
+
+ {/* Done pill */}
+
+
+
+
+
+
+ Done
+
+
+
+
+ {taskCounts.done || 0}
+
+
+
+
+
+
+
+ {/* Bottom bar with pinned indicator and actions - separate section */}
+
+ {/* Pinned indicator badge */}
+ {project.pinned ? (
+
+ DEFAULT
+
+ ) : (
+
+ )}
+
+ {/* Action Buttons - fixed to bottom right */}
+
onPin(e, project.id)}
+ onDelete={(e) => onDelete(e, project.id, project.title)}
+ />
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/components/ProjectCardActions.tsx b/archon-ui-main/src/features/projects/components/ProjectCardActions.tsx
new file mode 100644
index 0000000000..06a9f57d04
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/ProjectCardActions.tsx
@@ -0,0 +1,121 @@
+import { Clipboard, Pin, Trash2 } from "lucide-react";
+import type React from "react";
+import { useToast } from "../../ui/hooks/useToast";
+import { cn, glassmorphism } from "../../ui/primitives/styles";
+import { SimpleTooltip } from "../../ui/primitives/tooltip";
+
+interface ProjectCardActionsProps {
+ projectId: string;
+ projectTitle: string;
+ isPinned: boolean;
+ onPin: (e: React.MouseEvent) => void;
+ onDelete: (e: React.MouseEvent) => void;
+ isDeleting?: boolean;
+}
+
+export const ProjectCardActions: React.FC = ({
+ projectId,
+ projectTitle,
+ isPinned,
+ onPin,
+ onDelete,
+ isDeleting = false,
+}) => {
+ const { showToast } = useToast();
+
+ const handleCopyId = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ try {
+ await navigator.clipboard.writeText(projectId);
+ showToast("Project ID copied to clipboard", "success");
+ } catch {
+ // Fallback for older browsers
+ try {
+ const ta = document.createElement("textarea");
+ ta.value = projectId;
+ ta.style.position = "fixed";
+ ta.style.opacity = "0";
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy");
+ document.body.removeChild(ta);
+ showToast("Project ID copied to clipboard", "success");
+ } catch {
+ showToast("Failed to copy Project ID", "error");
+ }
+ }
+ };
+ return (
+
+ {/* Delete Button */}
+
+ {
+ e.stopPropagation();
+ if (!isDeleting) onDelete(e);
+ }}
+ disabled={isDeleting}
+ className={cn(
+ "w-5 h-5 rounded-full flex items-center justify-center",
+ "transition-all duration-300",
+ glassmorphism.priority.critical.background,
+ glassmorphism.priority.critical.text,
+ glassmorphism.priority.critical.hover,
+ glassmorphism.priority.critical.glow,
+ isDeleting && "opacity-50 cursor-not-allowed",
+ )}
+ aria-label={isDeleting ? "Deleting project..." : `Delete ${projectTitle}`}
+ >
+
+
+
+
+ {/* Pin Button */}
+
+ {
+ e.stopPropagation();
+ onPin(e);
+ }}
+ className={cn(
+ "w-5 h-5 rounded-full flex items-center justify-center",
+ "transition-all duration-300",
+ isPinned
+ ? "bg-purple-100/80 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400 hover:bg-purple-200 dark:hover:bg-purple-500/30 hover:shadow-[0_0_10px_rgba(168,85,247,0.3)]"
+ : glassmorphism.priority.medium.background +
+ " " +
+ glassmorphism.priority.medium.text +
+ " " +
+ glassmorphism.priority.medium.hover +
+ " " +
+ glassmorphism.priority.medium.glow,
+ )}
+ aria-label={isPinned ? "Unpin project" : "Pin as default"}
+ >
+
+
+
+
+ {/* Copy Project ID Button */}
+
+
+
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/components/ProjectHeader.tsx b/archon-ui-main/src/features/projects/components/ProjectHeader.tsx
new file mode 100644
index 0000000000..995792e0ab
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/ProjectHeader.tsx
@@ -0,0 +1,53 @@
+import { motion } from "framer-motion";
+import { Plus } from "lucide-react";
+import type React from "react";
+import { Button } from "../../ui/primitives/button";
+
+interface ProjectHeaderProps {
+ onNewProject: () => void;
+}
+
+const titleVariants = {
+ hidden: { opacity: 0, scale: 0.9 },
+ visible: {
+ opacity: 1,
+ scale: 1,
+ transition: { duration: 0.5, ease: [0.23, 1, 0.32, 1] },
+ },
+};
+
+const itemVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: [0.23, 1, 0.32, 1] },
+ },
+};
+
+export const ProjectHeader: React.FC = ({ onNewProject }) => {
+ return (
+
+
+
+ Projects
+
+
+
+ New Project
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/components/ProjectList.tsx b/archon-ui-main/src/features/projects/components/ProjectList.tsx
new file mode 100644
index 0000000000..6dceeb73f4
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/ProjectList.tsx
@@ -0,0 +1,118 @@
+import { motion } from "framer-motion";
+import { AlertCircle, Loader2 } from "lucide-react";
+import React from "react";
+import { Button } from "../../ui/primitives";
+import type { Project } from "../types";
+import { ProjectCard } from "./ProjectCard";
+
+interface ProjectListProps {
+ projects: Project[];
+ selectedProject: Project | null;
+ taskCounts: Record;
+ isLoading: boolean;
+ error: Error | null;
+ onProjectSelect: (project: Project) => void;
+ onPinProject: (e: React.MouseEvent, projectId: string) => void;
+ onDeleteProject: (e: React.MouseEvent, projectId: string, title: string) => void;
+ onRetry: () => void;
+}
+
+const itemVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: [0.23, 1, 0.32, 1] },
+ },
+};
+
+export const ProjectList: React.FC = ({
+ projects,
+ selectedProject,
+ taskCounts,
+ isLoading,
+ error,
+ onProjectSelect,
+ onPinProject,
+ onDeleteProject,
+ onRetry,
+}) => {
+ // Sort projects - pinned first, then by creation date (newest first)
+ const sortedProjects = React.useMemo(() => {
+ return [...projects].sort((a, b) => {
+ // Pinned projects always come first
+ if (a.pinned && !b.pinned) return -1;
+ if (!a.pinned && b.pinned) return 1;
+
+ // Then sort by creation date (newest first)
+ // This ensures new projects appear on the left after pinned ones
+ const timeA = Number.isFinite(Date.parse(a.created_at)) ? Date.parse(a.created_at) : 0;
+ const timeB = Number.isFinite(Date.parse(b.created_at)) ? Date.parse(b.created_at) : 0;
+ const byDate = timeB - timeA; // Newer first
+ return byDate !== 0 ? byDate : a.id.localeCompare(b.id); // Tie-break with ID for deterministic sort
+ });
+ }, [projects]);
+
+ if (isLoading) {
+ return (
+
+
+
+
+
Loading your projects...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
{error.message || "Failed to load projects"}
+
+ Try Again
+
+
+
+
+ );
+ }
+
+ if (sortedProjects.length === 0) {
+ return (
+
+
+
+
+ No projects yet. Create your first project to get started!
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {sortedProjects.map((project) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/components/index.ts b/archon-ui-main/src/features/projects/components/index.ts
new file mode 100644
index 0000000000..9da8308c3d
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Project Components
+ *
+ * All React components for the projects feature.
+ * Organized by sub-feature:
+ *
+ * - ProjectDashboard: Main project view orchestrator
+ * - ProjectManagement: Project CRUD, selection, metadata
+ * - TaskManagement: Task CRUD, status management
+ * - TaskBoard: Kanban board with drag-drop
+ * - TaskTable: Table view with filters/sorting
+ * - DocumentManagement: Project documents and editing
+ * - VersionHistory: Document versioning
+ */
+
+export { NewProjectModal } from "./NewProjectModal";
+export { ProjectCard } from "./ProjectCard";
+export { ProjectCardActions } from "./ProjectCardActions";
+export { ProjectHeader } from "./ProjectHeader";
+export { ProjectList } from "./ProjectList";
diff --git a/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx b/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx
new file mode 100644
index 0000000000..e119c7493b
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx
@@ -0,0 +1,189 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '../../../testing/test-utils';
+import { ProjectCard } from '../ProjectCard';
+import type { Project } from '../../types';
+
+describe('ProjectCard', () => {
+ const mockProject: Project = {
+ id: 'project-1',
+ title: 'Test Project',
+ description: 'Test Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: false,
+ features: [],
+ docs: [],
+ };
+
+ const mockTaskCounts = {
+ todo: 5,
+ doing: 3,
+ review: 2,
+ done: 10,
+ };
+
+ const mockHandlers = {
+ onSelect: vi.fn(),
+ onPin: vi.fn(),
+ onDelete: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render project title', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Test Project')).toBeInTheDocument();
+ });
+
+ it('should display task counts', () => {
+ render(
+
+ );
+
+ // Task count badges should be visible
+ // Note: Component only shows todo, doing, and done (not review)
+ const fives = screen.getAllByText('5');
+ expect(fives.length).toBeGreaterThan(0); // todo count
+ expect(screen.getByText('10')).toBeInTheDocument(); // done
+ // Doing count might be displayed as 3 or duplicated - implementation detail
+ });
+
+ it('should call onSelect when clicked', () => {
+ render(
+
+ );
+
+ const card = screen.getByRole('listitem');
+ fireEvent.click(card);
+
+ expect(mockHandlers.onSelect).toHaveBeenCalledWith(mockProject);
+ expect(mockHandlers.onSelect).toHaveBeenCalledTimes(1);
+ });
+
+ it('should apply selected styles when isSelected is true', () => {
+ const { container } = render(
+
+ );
+
+ const card = container.querySelector('[role="listitem"]');
+ // Check for selected-specific classes
+ expect(card?.className).toContain('scale-[1.02]');
+ expect(card?.className).toContain('border-purple');
+ });
+
+ it('should apply pinned styles when project is pinned', () => {
+ const pinnedProject = { ...mockProject, pinned: true };
+
+ const { container } = render(
+
+ );
+
+ const card = container.querySelector('[role="listitem"]');
+ // Check for pinned-specific classes
+ expect(card?.className).toContain('from-purple');
+ expect(card?.className).toContain('border-purple-500');
+ });
+
+ it('should render aurora glow effect when selected', () => {
+ const { container } = render(
+
+ );
+
+ // Aurora glow div should exist when selected
+ const glowEffect = container.querySelector('.animate-\\[pulse_8s_ease-in-out_infinite\\]');
+ expect(glowEffect).toBeInTheDocument();
+ });
+
+ it('should not render aurora glow effect when not selected', () => {
+ const { container } = render(
+
+ );
+
+ // Aurora glow div should not exist when not selected
+ const glowEffect = container.querySelector('.animate-\\[pulse_8s_ease-in-out_infinite\\]');
+ expect(glowEffect).not.toBeInTheDocument();
+ });
+
+ it('should show zero task counts correctly', () => {
+ const zeroTaskCounts = {
+ todo: 0,
+ doing: 0,
+ review: 0,
+ done: 0,
+ };
+
+ render(
+
+ );
+
+ // All counts should show 0 (ProjectCard may not show review count)
+ const zeros = screen.getAllByText('0');
+ expect(zeros.length).toBeGreaterThanOrEqual(3); // At least todo, doing, done
+ });
+
+ it('should handle very long project titles', () => {
+ const longTitleProject = {
+ ...mockProject,
+ title: 'This is an extremely long project title that should be truncated properly to avoid breaking the layout of the card component',
+ };
+
+ render(
+
+ );
+
+ const title = screen.getByText(/This is an extremely long project title/);
+ expect(title).toBeInTheDocument();
+ // Title should have line-clamp-2 class
+ expect(title.className).toContain('line-clamp-2');
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/projects/documents/DocsTab.tsx b/archon-ui-main/src/features/projects/documents/DocsTab.tsx
new file mode 100644
index 0000000000..4b49924421
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/DocsTab.tsx
@@ -0,0 +1,169 @@
+import { FileText, Search } from "lucide-react";
+import { useEffect, useState } from "react";
+import { Input } from "../../ui/primitives";
+import { cn } from "../../ui/primitives/styles";
+import { DocumentCard } from "./components/DocumentCard";
+import { DocumentViewer } from "./components/DocumentViewer";
+import { useProjectDocuments } from "./hooks";
+import type { ProjectDocument } from "./types";
+
+interface DocsTabProps {
+ project?: {
+ id: string;
+ title: string;
+ created_at?: string;
+ updated_at?: string;
+ } | null;
+}
+
+/**
+ * Read-only documents tab
+ * Displays existing documents from the project's JSONB field
+ */
+export const DocsTab = ({ project }: DocsTabProps) => {
+ const projectId = project?.id || "";
+
+ // Fetch documents from project's docs field
+ const { data: documents = [], isLoading } = useProjectDocuments(projectId);
+
+ // Document state
+ const [selectedDocument, setSelectedDocument] = useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+
+ // Auto-select first document when documents load
+ useEffect(() => {
+ if (documents.length > 0 && !selectedDocument) {
+ setSelectedDocument(documents[0]);
+ }
+ }, [documents, selectedDocument]);
+
+ // Update selected document if it was updated
+ useEffect(() => {
+ if (selectedDocument && documents.length > 0) {
+ const updated = documents.find((d) => d.id === selectedDocument.id);
+ if (updated && updated !== selectedDocument) {
+ setSelectedDocument(updated);
+ }
+ }
+ }, [documents, selectedDocument]);
+
+ // Filter documents based on search
+ const filteredDocuments = documents.filter((doc) => doc.title.toLowerCase().includes(searchQuery.toLowerCase()));
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Migration Warning Banner */}
+
+
+
+
+
+ Project Documents Under Migration
+
+
+ Editing and uploading project documents is currently disabled while we migrate to a new storage system.
+
+ {" "}
+ Please backup your existing project documents elsewhere as they will be lost when the migration is
+ complete.
+
+
+
+ Note: This only affects project-specific documents. Your knowledge base documents are safe and unaffected.
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Left Sidebar - Document List */}
+
+ {/* Header */}
+
+
+
+ Documents (Read-Only)
+
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9"
+ />
+
+
+ {/* Info message */}
+
+ Viewing {documents.length} document{documents.length !== 1 ? "s" : ""}
+
+
+
+ {/* Document List */}
+
+ {filteredDocuments.length === 0 ? (
+
+
+
{searchQuery ? "No documents found" : "No documents in this project"}
+
+ ) : (
+ filteredDocuments.map((doc) => (
+
{}} // No delete in read-only mode
+ />
+ ))
+ )}
+
+
+
+ {/* Right Content - Document Viewer */}
+
+ {selectedDocument ? (
+
+ ) : (
+
+
+
+
+ {documents.length > 0 ? "Select a document to view" : "No documents available"}
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx b/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx
new file mode 100644
index 0000000000..866f2e4ed1
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/components/DocumentCard.tsx
@@ -0,0 +1,171 @@
+import {
+ BookOpen,
+ Briefcase,
+ Clipboard,
+ Code,
+ Database,
+ FileCode,
+ FileText,
+ Info,
+ Rocket,
+ Users,
+ X,
+} from "lucide-react";
+import type React from "react";
+import { memo, useCallback, useState } from "react";
+import { Button } from "../../../ui/primitives";
+import type { DocumentCardProps, DocumentType } from "../types";
+
+const getDocumentIcon = (type?: DocumentType) => {
+ switch (type) {
+ case "prp":
+ return ;
+ case "technical":
+ return ;
+ case "business":
+ return ;
+ case "meeting_notes":
+ return ;
+ case "spec":
+ return ;
+ case "design":
+ return ;
+ case "api":
+ return ;
+ case "guide":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getTypeColor = (type?: DocumentType) => {
+ 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";
+ case "spec":
+ return "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/30";
+ case "design":
+ return "bg-pink-500/10 text-pink-600 dark:text-pink-400 border-pink-500/30";
+ case "api":
+ return "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-500/30";
+ case "guide":
+ return "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30";
+ default:
+ return "bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/30";
+ }
+};
+
+export const DocumentCard = memo(({ document, isActive, onSelect, onDelete }: DocumentCardProps) => {
+ const [showDelete, setShowDelete] = useState(false);
+ const [isCopied, setIsCopied] = useState(false);
+
+ const handleCopyId = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(document.id);
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), 2000);
+ },
+ [document.id],
+ );
+
+ const handleDelete = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onDelete(document);
+ },
+ [document, onDelete],
+ );
+
+ return (
+ // biome-ignore lint/a11y/useSemanticElements: Complex card with nested interactive elements - semantic button would break layout
+ {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onSelect(document);
+ }
+ }}
+ className={`
+ relative flex-shrink-0 w-48 p-4 rounded-lg cursor-pointer
+ transition-all duration-200 group
+ ${
+ isActive
+ ? "bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-500 shadow-lg scale-105"
+ : "bg-white/50 dark:bg-black/30 border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md"
+ }
+ `}
+ onClick={() => onSelect(document)}
+ onMouseEnter={() => setShowDelete(true)}
+ onMouseLeave={() => setShowDelete(false)}
+ >
+ {/* Document Type Badge */}
+
+ {getDocumentIcon(document.document_type as DocumentType)}
+ {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 */}
+
+
+ {document.id.slice(0, 8)}...
+
+
+ {isCopied ? (
+ ✓
+ ) : (
+
+ )}
+
+
+
+ {/* Delete Button */}
+ {showDelete && !isActive && (
+
+
+
+ )}
+
+ );
+});
+
+DocumentCard.displayName = "DocumentCard";
diff --git a/archon-ui-main/src/features/projects/documents/components/DocumentViewer.tsx b/archon-ui-main/src/features/projects/documents/components/DocumentViewer.tsx
new file mode 100644
index 0000000000..a447d98fcf
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/components/DocumentViewer.tsx
@@ -0,0 +1,115 @@
+import { FileText } from "lucide-react";
+import { cn } from "../../../ui/primitives/styles";
+import type { ProjectDocument } from "../types";
+
+interface DocumentViewerProps {
+ document: ProjectDocument;
+}
+
+/**
+ * Simple read-only document viewer
+ * Displays document content in a reliable way without complex editing
+ */
+export const DocumentViewer = ({ document }: DocumentViewerProps) => {
+ // Extract content for display
+ const renderContent = () => {
+ if (!document.content) {
+ return No content available
;
+ }
+
+ // Handle string content
+ if (typeof document.content === "string") {
+ return (
+ {document.content}
+ );
+ }
+
+ // Handle markdown field
+ if ("markdown" in document.content && typeof document.content.markdown === "string") {
+ return (
+
+
+ {document.content.markdown}
+
+
+ );
+ }
+
+ // Handle text field
+ if ("text" in document.content && typeof document.content.text === "string") {
+ return (
+
+ {document.content.text}
+
+ );
+ }
+
+ // Handle structured content (JSON)
+ return (
+
+ {Object.entries(document.content).map(([key, value]) => (
+
+
+ {key.replace(/_/g, " ").charAt(0).toUpperCase() + key.replace(/_/g, " ").slice(1)}
+
+
+ {typeof value === "string" ? (
+
{value}
+ ) : Array.isArray(value) ? (
+
+ {value.map((item, i) => (
+
+ {typeof item === "object" ? JSON.stringify(item, null, 2) : String(item)}
+
+ ))}
+
+ ) : (
+
+ {JSON.stringify(value, null, 2)}
+
+ )}
+
+
+ ))}
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
{document.title}
+
+ Type: {document.document_type || "document"} • Last updated:{" "}
+ {new Date(document.updated_at).toLocaleDateString()}
+
+
+
+ {document.tags && document.tags.length > 0 && (
+
+ {document.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+ {/* Content */}
+
{renderContent()}
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/documents/components/index.ts b/archon-ui-main/src/features/projects/documents/components/index.ts
new file mode 100644
index 0000000000..f1377f08df
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/components/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Document Management Components
+ *
+ * Components for document display and management following vertical slice architecture.
+ * Uses Radix UI primitives for better accessibility and consistency.
+ */
+
+export { DocumentCard } from "./DocumentCard";
+export { DocumentViewer } from "./DocumentViewer";
diff --git a/archon-ui-main/src/features/projects/documents/hooks/index.ts b/archon-ui-main/src/features/projects/documents/hooks/index.ts
new file mode 100644
index 0000000000..7904256801
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/hooks/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Document Hooks
+ *
+ * Read-only hooks for document display
+ */
+
+export { useProjectDocuments } from "./useDocumentQueries";
diff --git a/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts b/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts
new file mode 100644
index 0000000000..fc9ba52524
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts
@@ -0,0 +1,24 @@
+import { useQuery } from "@tanstack/react-query";
+import { projectService } from "../../services";
+import type { ProjectDocument } from "../types";
+
+// Query keys
+const documentKeys = {
+ all: (projectId: string) => ["projects", projectId, "documents"] as const,
+};
+
+/**
+ * Get documents from project's docs JSONB field
+ * Read-only - no mutations
+ */
+export function useProjectDocuments(projectId: string | undefined) {
+ return useQuery({
+ queryKey: projectId ? documentKeys.all(projectId) : ["documents-undefined"],
+ queryFn: async () => {
+ if (!projectId) return [];
+ const project = await projectService.getProject(projectId);
+ return (project.docs || []) as ProjectDocument[];
+ },
+ enabled: !!projectId,
+ });
+}
diff --git a/archon-ui-main/src/features/projects/documents/index.ts b/archon-ui-main/src/features/projects/documents/index.ts
new file mode 100644
index 0000000000..0b7b97ea11
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Documents Feature Module
+ *
+ * Sub-feature of projects for managing project documentation
+ */
+
+export { DocsTab } from "./DocsTab";
diff --git a/archon-ui-main/src/features/projects/documents/types/document.ts b/archon-ui-main/src/features/projects/documents/types/document.ts
new file mode 100644
index 0000000000..0e86169c0d
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/types/document.ts
@@ -0,0 +1,45 @@
+/**
+ * Document Type Definitions
+ *
+ * Core types for document management within projects.
+ */
+
+// Document content can be structured in various ways
+export type DocumentContent =
+ | string // Plain text or markdown
+ | { markdown: string } // Markdown content
+ | { text: string } // Text content
+ | {
+ markdown?: string;
+ text?: string;
+ [key: string]: unknown; // Allow other fields but with known type
+ } // Mixed content
+ | Record; // Generic object content
+
+export interface ProjectDocument {
+ id: string;
+ title: string;
+ content?: DocumentContent;
+ document_type?: DocumentType | string;
+ tags?: string[];
+ updated_at: string;
+ created_at?: string;
+}
+
+export type DocumentType =
+ | "prp"
+ | "technical"
+ | "business"
+ | "meeting_notes"
+ | "spec"
+ | "design"
+ | "note"
+ | "api"
+ | "guide";
+
+export interface DocumentCardProps {
+ document: ProjectDocument;
+ isActive: boolean;
+ onSelect: (doc: ProjectDocument) => void;
+ onDelete: (doc: ProjectDocument) => void;
+}
diff --git a/archon-ui-main/src/features/projects/documents/types/index.ts b/archon-ui-main/src/features/projects/documents/types/index.ts
new file mode 100644
index 0000000000..5cb5565985
--- /dev/null
+++ b/archon-ui-main/src/features/projects/documents/types/index.ts
@@ -0,0 +1,8 @@
+/**
+ * Document Types
+ *
+ * All document-related types for the projects feature.
+ */
+
+// Document types
+export type { DocumentCardProps, DocumentType, ProjectDocument } from "./document";
diff --git a/archon-ui-main/src/features/projects/hooks/index.ts b/archon-ui-main/src/features/projects/hooks/index.ts
new file mode 100644
index 0000000000..14f21d0536
--- /dev/null
+++ b/archon-ui-main/src/features/projects/hooks/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Project Hooks
+ *
+ * All React hooks for the projects feature.
+ * Includes:
+ * - Data fetching hooks (useProjects, useTasks, useDocuments)
+ * - Mutation hooks (useCreateProject, useUpdateTask, etc.)
+ * - UI state hooks (useProjectSelection, useTaskFilters)
+ * - Business logic hooks (useTaskDragDrop, useDocumentEditor)
+ */
+
+export {
+ projectKeys,
+ useCreateProject,
+ useDeleteProject,
+ useProjectFeatures,
+ useProjects,
+ useTaskCounts,
+ useUpdateProject,
+} from "./useProjectQueries";
diff --git a/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts b/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts
new file mode 100644
index 0000000000..0b90ba957b
--- /dev/null
+++ b/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts
@@ -0,0 +1,208 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { projectKeys, useProjects, useCreateProject, useUpdateProject, useDeleteProject } from '../useProjectQueries';
+import type { Project } from '../../types';
+import React from 'react';
+
+// Mock the services
+vi.mock('../../services', () => ({
+ projectService: {
+ listProjects: vi.fn(),
+ createProject: vi.fn(),
+ updateProject: vi.fn(),
+ deleteProject: vi.fn(),
+ getProjectFeatures: vi.fn(),
+ },
+ taskService: {
+ getTaskCountsForAllProjects: vi.fn(),
+ },
+}));
+
+// Mock the toast hook
+vi.mock('../../../ui/hooks/useToast', () => ({
+ useToast: () => ({
+ showToast: vi.fn(),
+ }),
+}));
+
+// Mock smart polling
+vi.mock('../../../ui/hooks', () => ({
+ useSmartPolling: () => ({
+ refetchInterval: 5000,
+ isPaused: false,
+ }),
+}));
+
+// Test wrapper with QueryClient
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ return ({ children }: { children: React.ReactNode }) =>
+ React.createElement(QueryClientProvider, { client: queryClient }, children);
+};
+
+describe('useProjectQueries', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('projectKeys', () => {
+ it('should generate correct query keys', () => {
+ expect(projectKeys.all).toEqual(['projects']);
+ expect(projectKeys.lists()).toEqual(['projects', 'list']);
+ expect(projectKeys.detail('123')).toEqual(['projects', 'detail', '123']);
+ expect(projectKeys.tasks('123')).toEqual(['projects', 'detail', '123', 'tasks']);
+ expect(projectKeys.features('123')).toEqual(['projects', 'detail', '123', 'features']);
+ expect(projectKeys.documents('123')).toEqual(['projects', 'detail', '123', 'documents']);
+ });
+ });
+
+ describe('useProjects', () => {
+ it('should fetch projects list', async () => {
+ const mockProjects: Project[] = [
+ {
+ id: '1',
+ title: 'Test Project',
+ description: 'Test Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: false,
+ features: [],
+ docs: [],
+ },
+ ];
+
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.listProjects).mockResolvedValue(mockProjects);
+
+ const { result } = renderHook(() => useProjects(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockProjects);
+ });
+
+ expect(projectService.listProjects).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('useCreateProject', () => {
+ it('should optimistically add project and replace with server response', async () => {
+ const newProject: Project = {
+ id: 'real-id',
+ title: 'New Project',
+ description: 'New Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: false,
+ features: [],
+ docs: [],
+ };
+
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.createProject).mockResolvedValue({
+ project: newProject,
+ message: 'Created',
+ });
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateProject(), { wrapper });
+
+ await result.current.mutateAsync({
+ title: 'New Project',
+ description: 'New Description',
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(projectService.createProject).toHaveBeenCalledWith({
+ title: 'New Project',
+ description: 'New Description',
+ });
+ });
+ });
+
+ it('should rollback on error', async () => {
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.createProject).mockRejectedValue(new Error('Network error'));
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateProject(), { wrapper });
+
+ await expect(
+ result.current.mutateAsync({
+ title: 'New Project',
+ description: 'New Description',
+ })
+ ).rejects.toThrow('Network error');
+ });
+ });
+
+ describe('useUpdateProject', () => {
+ it('should handle pinning a project', async () => {
+ const updatedProject: Project = {
+ id: '1',
+ title: 'Test Project',
+ description: 'Test Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: true,
+ features: [],
+ docs: [],
+ };
+
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.updateProject).mockResolvedValue(updatedProject);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useUpdateProject(), { wrapper });
+
+ await result.current.mutateAsync({
+ projectId: '1',
+ updates: { pinned: true },
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(projectService.updateProject).toHaveBeenCalledWith('1', { pinned: true });
+ });
+ });
+ });
+
+ describe('useDeleteProject', () => {
+ it('should optimistically remove project', async () => {
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.deleteProject).mockResolvedValue(undefined);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useDeleteProject(), { wrapper });
+
+ await result.current.mutateAsync('project-to-delete');
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(projectService.deleteProject).toHaveBeenCalledWith('project-to-delete');
+ });
+ });
+
+ it('should rollback on delete error', async () => {
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.deleteProject).mockRejectedValue(new Error('Permission denied'));
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useDeleteProject(), { wrapper });
+
+ await expect(
+ result.current.mutateAsync('project-to-delete')
+ ).rejects.toThrow('Permission denied');
+ });
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts b/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts
new file mode 100644
index 0000000000..e67b6c34e6
--- /dev/null
+++ b/archon-ui-main/src/features/projects/hooks/useProjectQueries.ts
@@ -0,0 +1,222 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useSmartPolling } from "../../ui/hooks";
+import { useToast } from "../../ui/hooks/useToast";
+import { projectService, taskService } from "../services";
+import type { CreateProjectRequest, Project, UpdateProjectRequest } from "../types";
+
+// Query keys factory for better organization
+export const projectKeys = {
+ all: ["projects"] as const,
+ lists: () => [...projectKeys.all, "list"] as const,
+ list: (filters?: unknown) => [...projectKeys.lists(), filters] as const,
+ details: () => [...projectKeys.all, "detail"] as const,
+ detail: (id: string) => [...projectKeys.details(), id] as const,
+ tasks: (projectId: string) => [...projectKeys.detail(projectId), "tasks"] as const,
+ taskCounts: () => ["taskCounts"] as const,
+ features: (projectId: string) => [...projectKeys.detail(projectId), "features"] as const,
+ documents: (projectId: string) => [...projectKeys.detail(projectId), "documents"] as const,
+};
+
+// Fetch all projects with smart polling
+export function useProjects() {
+ const { refetchInterval } = useSmartPolling(20000); // 20 second base interval for projects
+
+ return useQuery({
+ queryKey: projectKeys.lists(),
+ queryFn: () => projectService.listProjects(),
+ refetchInterval, // Smart interval based on page visibility/focus
+ refetchOnWindowFocus: false, // Avoid double refetch with polling
+ staleTime: 15000, // Consider data stale after 15 seconds
+ });
+}
+
+// Fetch task counts for all projects
+export function useTaskCounts() {
+ return useQuery>>({
+ queryKey: projectKeys.taskCounts(),
+ queryFn: () => taskService.getTaskCountsForAllProjects(),
+ refetchInterval: false, // Don't poll, only refetch manually
+ staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+ });
+}
+
+// Fetch project features
+export function useProjectFeatures(projectId: string | undefined) {
+ return useQuery({
+ queryKey: projectId ? projectKeys.features(projectId) : ["features-undefined"],
+ queryFn: () => (projectId ? projectService.getProjectFeatures(projectId) : Promise.reject("No project ID")),
+ enabled: !!projectId,
+ staleTime: 30000, // Cache for 30 seconds
+ });
+}
+
+// Create project mutation with optimistic updates
+export function useCreateProject() {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+
+ return useMutation({
+ mutationFn: (projectData: CreateProjectRequest) => projectService.createProject(projectData),
+ onMutate: async (newProjectData) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: projectKeys.lists() });
+
+ // Snapshot the previous value
+ const previousProjects = queryClient.getQueryData(projectKeys.lists());
+
+ // Create optimistic project with temporary ID
+ const tempId = `temp-${Date.now()}`;
+ const optimisticProject: Project = {
+ id: tempId, // Temporary ID until real one comes back
+ title: newProjectData.title,
+ description: newProjectData.description,
+ github_repo: newProjectData.github_repo,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ prd: undefined,
+ features: [],
+ data: undefined,
+ docs: [],
+ pinned: false,
+ };
+
+ // Optimistically add the new project
+ queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => {
+ if (!old) return [optimisticProject];
+ // Add new project at the beginning of the list
+ return [optimisticProject, ...old];
+ });
+
+ return { previousProjects, tempId };
+ },
+ onError: (error, variables, context) => {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ console.error("Failed to create project:", error, { variables });
+
+ // Rollback on error
+ if (context?.previousProjects) {
+ queryClient.setQueryData(projectKeys.lists(), context.previousProjects);
+ }
+
+ showToast(`Failed to create project: ${errorMessage}`, "error");
+ },
+ onSuccess: (response, _variables, context) => {
+ // Extract the actual project from the response
+ const newProject = response.project;
+
+ // Replace optimistic project with real one from server
+ queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => {
+ if (!old) return [newProject];
+ // Replace only the specific temp project with real one
+ return old
+ .map((project) => (project.id === context?.tempId ? newProject : project))
+ .filter(
+ (project, index, self) =>
+ // Remove any duplicates just in case
+ index === self.findIndex((p) => p.id === project.id),
+ );
+ });
+
+ showToast("Project created successfully!", "success");
+ },
+ onSettled: () => {
+ // Always refetch to ensure consistency after operation completes
+ queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
+ },
+ });
+}
+
+// Update project mutation (for pinning, etc.)
+export function useUpdateProject() {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+
+ return useMutation({
+ mutationFn: ({ projectId, updates }: { projectId: string; updates: UpdateProjectRequest }) =>
+ projectService.updateProject(projectId, updates),
+ onMutate: async ({ projectId, updates }) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: projectKeys.lists() });
+
+ // Snapshot the previous value
+ const previousProjects = queryClient.getQueryData(projectKeys.lists());
+
+ // Optimistically update
+ queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => {
+ if (!old) return old;
+
+ // If pinning a project, unpin all others first
+ if (updates.pinned === true) {
+ return old.map((p) => ({
+ ...p,
+ pinned: p.id === projectId,
+ }));
+ }
+
+ return old.map((p) => (p.id === projectId ? { ...p, ...updates } : p));
+ });
+
+ return { previousProjects };
+ },
+ onError: (_err, _variables, context) => {
+ // Rollback on error
+ if (context?.previousProjects) {
+ queryClient.setQueryData(projectKeys.lists(), context.previousProjects);
+ }
+ showToast("Failed to update project", "error");
+ },
+ onSuccess: (data, variables) => {
+ // Invalidate and refetch
+ queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
+
+ if (variables.updates.pinned !== undefined) {
+ const message = variables.updates.pinned
+ ? `Pinned "${data.title}" as default project`
+ : `Removed "${data.title}" from default selection`;
+ showToast(message, "info");
+ }
+ },
+ });
+}
+
+// Delete project mutation with optimistic updates
+export function useDeleteProject() {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+
+ return useMutation({
+ mutationFn: (projectId: string) => projectService.deleteProject(projectId),
+ onMutate: async (projectId) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: projectKeys.lists() });
+
+ // Snapshot the previous value
+ const previousProjects = queryClient.getQueryData(projectKeys.lists());
+
+ // Optimistically remove the project
+ queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => {
+ if (!old) return old;
+ return old.filter((project) => project.id !== projectId);
+ });
+
+ return { previousProjects };
+ },
+ onError: (error, projectId, context) => {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ console.error("Failed to delete project:", error, { projectId });
+
+ // Rollback on error
+ if (context?.previousProjects) {
+ queryClient.setQueryData(projectKeys.lists(), context.previousProjects);
+ }
+
+ showToast(`Failed to delete project: ${errorMessage}`, "error");
+ },
+ onSuccess: (_, projectId) => {
+ // Don't refetch on success - trust optimistic update
+ // Only remove the specific project's detail data (including nested keys)
+ queryClient.removeQueries({ queryKey: projectKeys.detail(projectId), exact: false });
+ showToast("Project deleted successfully", "success");
+ },
+ });
+}
diff --git a/archon-ui-main/src/features/projects/index.ts b/archon-ui-main/src/features/projects/index.ts
new file mode 100644
index 0000000000..c8e50df70a
--- /dev/null
+++ b/archon-ui-main/src/features/projects/index.ts
@@ -0,0 +1,22 @@
+/**
+ * Projects Feature Module
+ *
+ * Vertical slice containing all project-related functionality:
+ * - Project management (CRUD, selection)
+ * - Task management (CRUD, status, board, table views)
+ * - Document management (docs, versioning)
+ * - Project dashboard and routing
+ */
+
+// Components
+export * from "./components";
+export * from "./documents";
+
+// Hooks
+export * from "./hooks";
+
+// Sub-features
+export * from "./tasks";
+// Views
+export { ProjectsView } from "./views/ProjectsView";
+export { ProjectsViewWithBoundary } from "./views/ProjectsViewWithBoundary";
diff --git a/archon-ui-main/src/features/projects/schemas/index.ts b/archon-ui-main/src/features/projects/schemas/index.ts
new file mode 100644
index 0000000000..b43cfcedf3
--- /dev/null
+++ b/archon-ui-main/src/features/projects/schemas/index.ts
@@ -0,0 +1,62 @@
+import { z } from "zod";
+
+// Base validation schemas
+export const ProjectColorSchema = z.enum(["cyan", "purple", "pink", "blue", "orange", "green"]);
+
+// Project schemas
+export const CreateProjectSchema = z.object({
+ title: z.string().min(1, "Project title is required").max(255, "Project title must be less than 255 characters"),
+ description: z.string().max(1000, "Description must be less than 1000 characters").optional(),
+ icon: z.string().optional(),
+ color: ProjectColorSchema.optional(),
+ github_repo: z.string().url("GitHub repo must be a valid URL").optional(),
+ prd: z.record(z.unknown()).optional(),
+ docs: z.array(z.unknown()).optional(),
+ features: z.array(z.unknown()).optional(),
+ data: z.array(z.unknown()).optional(),
+ technical_sources: z.array(z.string()).optional(),
+ business_sources: z.array(z.string()).optional(),
+ pinned: z.boolean().optional(),
+});
+
+export const UpdateProjectSchema = CreateProjectSchema.partial();
+
+export const ProjectSchema = z.object({
+ id: z.string().uuid("Project ID must be a valid UUID"),
+ title: z.string().min(1),
+ prd: z.record(z.unknown()).optional(),
+ docs: z.array(z.unknown()).optional(),
+ features: z.array(z.unknown()).optional(),
+ data: z.array(z.unknown()).optional(),
+ github_repo: z.string().url().optional().or(z.literal("")),
+ created_at: z.string().datetime(),
+ updated_at: z.string().datetime(),
+ technical_sources: z.array(z.unknown()).optional(), // Can be strings or full objects
+ business_sources: z.array(z.unknown()).optional(), // Can be strings or full objects
+
+ // Extended UI properties
+ description: z.string().optional(),
+ icon: z.string().optional(),
+ color: ProjectColorSchema.optional(),
+ progress: z.number().min(0).max(100).optional(),
+ pinned: z.boolean(),
+ updated: z.string().optional(), // Human-readable format
+});
+
+// Validation helper functions
+export function validateProject(data: unknown) {
+ return ProjectSchema.safeParse(data);
+}
+
+export function validateCreateProject(data: unknown) {
+ return CreateProjectSchema.safeParse(data);
+}
+
+export function validateUpdateProject(data: unknown) {
+ return UpdateProjectSchema.safeParse(data);
+}
+
+// Export type inference helpers
+export type CreateProjectInput = z.infer;
+export type UpdateProjectInput = z.infer;
+export type ProjectInput = z.infer;
diff --git a/archon-ui-main/src/features/projects/services/index.ts b/archon-ui-main/src/features/projects/services/index.ts
new file mode 100644
index 0000000000..ef953ff6e5
--- /dev/null
+++ b/archon-ui-main/src/features/projects/services/index.ts
@@ -0,0 +1,13 @@
+/**
+ * Project Services
+ *
+ * All API communication and business logic for the projects feature.
+ * Replaces the monolithic src/services/projectService.ts with focused services.
+ */
+
+// Export shared utilities
+export * from "../shared/api";
+// Re-export other services for convenience
+export { taskService } from "../tasks/services/taskService";
+// Export project-specific services
+export { projectService } from "./projectService";
diff --git a/archon-ui-main/src/features/projects/services/projectService.ts b/archon-ui-main/src/features/projects/services/projectService.ts
new file mode 100644
index 0000000000..1eb7c81beb
--- /dev/null
+++ b/archon-ui-main/src/features/projects/services/projectService.ts
@@ -0,0 +1,188 @@
+/**
+ * Project Management Service
+ * Focused service for project CRUD operations only
+ */
+
+import { validateCreateProject, validateUpdateProject } from "../schemas";
+import { formatRelativeTime, formatZodErrors, ValidationError } from "../shared/api";
+import { callAPIWithETag, invalidateETagCache } from "../shared/apiWithEtag";
+import type { CreateProjectRequest, Project, ProjectFeatures, UpdateProjectRequest } from "../types";
+
+export const projectService = {
+ /**
+ * Get all projects
+ */
+ async listProjects(): Promise {
+ try {
+ // Fetching projects from API
+ const response = await callAPIWithETag<{ projects: Project[] }>("/api/projects");
+ // API response received
+
+ const projects = response.projects || [];
+ // Processing projects array
+
+ // Process raw pinned values
+
+ // Add computed UI properties
+ const processedProjects = projects.map((project: Project) => {
+ // Process the raw pinned value
+
+ const processed = {
+ ...project,
+ // Ensure pinned is properly handled as boolean
+ pinned: project.pinned === true,
+ progress: project.progress || 0,
+ updated: project.updated || formatRelativeTime(project.updated_at),
+ };
+ return processed;
+ });
+
+ // All projects processed
+ return processedProjects;
+ } catch (error) {
+ console.error("Failed to list projects:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get a specific project by ID
+ */
+ async getProject(projectId: string): Promise {
+ try {
+ const project = await callAPIWithETag(`/api/projects/${projectId}`);
+
+ return {
+ ...project,
+ progress: project.progress || 0,
+ updated: project.updated || formatRelativeTime(project.updated_at),
+ };
+ } catch (error) {
+ console.error(`Failed to get project ${projectId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Create a new project
+ */
+ async createProject(projectData: CreateProjectRequest): Promise<{
+ project_id: string;
+ project: Project;
+ status: string;
+ message: string;
+ }> {
+ // Validate input
+ // Validate project data
+ const validation = validateCreateProject(projectData);
+ if (!validation.success) {
+ // Validation failed
+ throw new ValidationError(formatZodErrors(validation.error));
+ }
+ // Validation passed
+
+ try {
+ // Sending project creation request
+ const response = await callAPIWithETag<{
+ project_id: string;
+ project: Project;
+ status: string;
+ message: string;
+ }>("/api/projects", {
+ method: "POST",
+ body: JSON.stringify(validation.data),
+ });
+
+ // Invalidate project list cache after creation
+ invalidateETagCache("/api/projects");
+
+ // Project creation response received
+ return response;
+ } catch (error) {
+ console.error("[PROJECT SERVICE] Failed to initiate project creation:", error);
+ if (error instanceof Error) {
+ console.error("[PROJECT SERVICE] Error details:", {
+ message: error.message,
+ name: error.name,
+ });
+ }
+ throw error;
+ }
+ },
+
+ /**
+ * Update an existing project
+ */
+ async updateProject(projectId: string, updates: UpdateProjectRequest): Promise {
+ // Validate input
+ // Updating project with provided data
+ const validation = validateUpdateProject(updates);
+ if (!validation.success) {
+ // Validation failed
+ throw new ValidationError(formatZodErrors(validation.error));
+ }
+
+ try {
+ // Sending update request to API
+ const project = await callAPIWithETag(`/api/projects/${projectId}`, {
+ method: "PUT",
+ body: JSON.stringify(validation.data),
+ });
+
+ // Invalidate caches after update
+ invalidateETagCache("/api/projects");
+ invalidateETagCache(`/api/projects/${projectId}`);
+
+ // API update response received
+
+ // Ensure pinned property is properly handled as boolean
+ const processedProject = {
+ ...project,
+ pinned: project.pinned === true,
+ progress: project.progress || 0,
+ updated: formatRelativeTime(project.updated_at),
+ };
+
+ // Project update processed
+
+ return processedProject;
+ } catch (error) {
+ console.error(`Failed to update project ${projectId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Delete a project
+ */
+ async deleteProject(projectId: string): Promise {
+ try {
+ await callAPIWithETag(`/api/projects/${projectId}`, {
+ method: "DELETE",
+ });
+
+ // Invalidate caches after deletion
+ invalidateETagCache("/api/projects");
+ invalidateETagCache(`/api/projects/${projectId}`);
+ } catch (error) {
+ console.error(`Failed to delete project ${projectId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get features from a project's features JSONB field
+ */
+ async getProjectFeatures(projectId: string): Promise<{ features: ProjectFeatures; count: number }> {
+ try {
+ const response = await callAPIWithETag<{
+ features: ProjectFeatures;
+ count: number;
+ }>(`/api/projects/${projectId}/features`);
+ return response;
+ } catch (error) {
+ console.error(`Failed to get features for project ${projectId}:`, error);
+ throw error;
+ }
+ },
+};
diff --git a/archon-ui-main/src/features/projects/shared/api.ts b/archon-ui-main/src/features/projects/shared/api.ts
new file mode 100644
index 0000000000..ec50acde1a
--- /dev/null
+++ b/archon-ui-main/src/features/projects/shared/api.ts
@@ -0,0 +1,130 @@
+/**
+ * Shared API utilities for project features
+ * Common error handling and API calling functions
+ */
+
+// API configuration - use relative URL to go through Vite proxy
+const API_BASE_URL = "/api";
+
+// Error classes
+export class ProjectServiceError extends Error {
+ constructor(
+ message: string,
+ public code?: string,
+ public statusCode?: number,
+ ) {
+ super(message);
+ this.name = "ProjectServiceError";
+ }
+}
+
+export class ValidationError extends ProjectServiceError {
+ constructor(message: string) {
+ super(message, "VALIDATION_ERROR", 400);
+ this.name = "ValidationError";
+ }
+}
+
+export class MCPToolError extends ProjectServiceError {
+ constructor(
+ message: string,
+ public toolName: string,
+ ) {
+ super(message, "MCP_TOOL_ERROR", 500);
+ this.name = "MCPToolError";
+ }
+}
+
+// Helper function to format validation errors
+interface ValidationErrorDetail {
+ path: string[];
+ message: string;
+}
+
+interface ValidationErrorObject {
+ errors: ValidationErrorDetail[];
+}
+
+export function formatValidationErrors(errors: ValidationErrorObject): string {
+ return errors.errors.map((error: ValidationErrorDetail) => `${error.path.join(".")}: ${error.message}`).join(", ");
+}
+
+// Helper to convert Zod errors to ValidationErrorObject format
+export function formatZodErrors(zodError: { issues: Array<{ path: (string | number)[]; message: string }> }): string {
+ const validationErrors: ValidationErrorObject = {
+ errors: zodError.issues.map((issue) => ({
+ path: issue.path.map(String),
+ message: issue.message,
+ })),
+ };
+ return formatValidationErrors(validationErrors);
+}
+
+// Helper function to call FastAPI endpoints directly
+export async function callAPI(endpoint: string, options: RequestInit = {}): Promise {
+ try {
+ // Remove /api prefix if it exists since API_BASE_URL already includes it
+ const cleanEndpoint = endpoint.startsWith("/api") ? endpoint.substring(4) : endpoint;
+ const response = await fetch(`${API_BASE_URL}${cleanEndpoint}`, {
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ ...options,
+ });
+
+ if (!response.ok) {
+ // Try to get error details from response body
+ let errorMessage = `HTTP error! status: ${response.status}`;
+ try {
+ const errorBody = await response.text();
+ if (errorBody) {
+ const errorJson = JSON.parse(errorBody);
+ errorMessage = errorJson.detail || errorJson.error || errorMessage;
+ }
+ } catch (_e) {
+ // Ignore parse errors, use default message
+ }
+
+ throw new ProjectServiceError(errorMessage, "HTTP_ERROR", response.status);
+ }
+
+ // Handle 204 No Content responses (common for DELETE operations)
+ if (response.status === 204) {
+ return undefined as T;
+ }
+
+ const result = await response.json();
+
+ // Check if response has error field (from FastAPI error format)
+ if (result.error) {
+ throw new ProjectServiceError(result.error, "API_ERROR", response.status);
+ }
+
+ return result as T;
+ } catch (error) {
+ if (error instanceof ProjectServiceError) {
+ throw error;
+ }
+
+ throw new ProjectServiceError(
+ `Failed to call API ${endpoint}: ${error instanceof Error ? error.message : "Unknown error"}`,
+ "NETWORK_ERROR",
+ 500,
+ );
+ }
+}
+
+// Utility function for relative time formatting
+export function formatRelativeTime(dateString: string): string {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
+
+ if (diffInSeconds < 60) return "just now";
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
+ if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
+
+ return `${Math.floor(diffInSeconds / 604800)} weeks ago`;
+}
diff --git a/archon-ui-main/src/features/projects/shared/apiWithEtag.ts b/archon-ui-main/src/features/projects/shared/apiWithEtag.ts
new file mode 100644
index 0000000000..f9956420aa
--- /dev/null
+++ b/archon-ui-main/src/features/projects/shared/apiWithEtag.ts
@@ -0,0 +1,160 @@
+/**
+ * ETag-aware API client for TanStack Query integration
+ * Reduces bandwidth by 70-90% through HTTP 304 responses
+ */
+
+import { ProjectServiceError } from "./api";
+
+// API configuration
+const API_BASE_URL = "/api";
+
+// ETag and data cache stores
+const etagCache = new Map();
+const dataCache = new Map();
+
+// Generate cache key from endpoint and options
+function getCacheKey(endpoint: string, options: RequestInit = {}): string {
+ // Include method in cache key (GET vs POST, etc)
+ const method = options.method || "GET";
+ return `${method}:${endpoint}`;
+}
+
+/**
+ * ETag-aware API call function
+ * Handles 304 Not Modified responses by returning cached data
+ */
+export async function callAPIWithETag(endpoint: string, options: RequestInit = {}): Promise {
+ try {
+ // Clean endpoint
+ const cleanEndpoint = endpoint.startsWith("/api") ? endpoint.substring(4) : endpoint;
+ const fullUrl = `${API_BASE_URL}${cleanEndpoint}`;
+ const cacheKey = getCacheKey(fullUrl, options);
+
+ // Get stored ETag for this endpoint
+ const storedEtag = etagCache.get(cacheKey);
+
+ // Build headers with If-None-Match if we have an ETag
+ const headers: Record = {
+ "Content-Type": "application/json",
+ ...(options.headers as Record),
+ };
+
+ // Only add If-None-Match for GET requests
+ if (storedEtag && (!options.method || options.method === "GET")) {
+ headers["If-None-Match"] = storedEtag;
+ }
+
+ // Make the request
+ const response = await fetch(fullUrl, {
+ ...options,
+ headers,
+ });
+
+ // Handle 304 Not Modified - return cached data
+ if (response.status === 304) {
+ const cachedData = dataCache.get(cacheKey);
+ if (cachedData) {
+ // Console log for debugging (can be removed in production)
+ console.log(`%c[ETag] Cache hit (304) for ${cleanEndpoint}`, "color: #10b981; font-weight: bold");
+ return cachedData as T;
+ }
+ // If no cached data (shouldn't happen), make a fresh request
+ console.warn(`[ETag] 304 received but no cached data for ${cleanEndpoint}`);
+ // Fall through to handle as a normal response
+ }
+
+ // Handle errors
+ if (!response.ok && response.status !== 304) {
+ let errorMessage = `HTTP error! status: ${response.status}`;
+ try {
+ const errorBody = await response.text();
+ if (errorBody) {
+ const errorJson = JSON.parse(errorBody);
+ errorMessage = errorJson.detail || errorJson.error || errorMessage;
+ }
+ } catch (_e) {
+ // Ignore parse errors
+ }
+ throw new ProjectServiceError(errorMessage, "HTTP_ERROR", response.status);
+ }
+
+ // Handle 204 No Content (DELETE operations)
+ if (response.status === 204) {
+ // Clear caches for this endpoint on successful deletion
+ etagCache.delete(cacheKey);
+ dataCache.delete(cacheKey);
+ return undefined as T;
+ }
+
+ // Parse response data
+ const result = await response.json();
+
+ // Check for API errors
+ if (result.error) {
+ throw new ProjectServiceError(result.error, "API_ERROR", response.status);
+ }
+
+ // Store ETag if present (only for GET requests)
+ const newEtag = response.headers.get("ETag");
+ if (newEtag && (!options.method || options.method === "GET")) {
+ etagCache.set(cacheKey, newEtag);
+ // Store the data along with ETag
+ dataCache.set(cacheKey, result);
+ console.log(
+ `%c[ETag] Cached new data for ${cleanEndpoint}`,
+ "color: #3b82f6; font-weight: bold",
+ `ETag: ${newEtag.substring(0, 12)}...`,
+ );
+ }
+
+ return result as T;
+ } catch (error) {
+ if (error instanceof ProjectServiceError) {
+ throw error;
+ }
+
+ throw new ProjectServiceError(
+ `Failed to call API ${endpoint}: ${error instanceof Error ? error.message : "Unknown error"}`,
+ "NETWORK_ERROR",
+ 500,
+ );
+ }
+}
+
+/**
+ * Clear ETag caches - useful for logout or data refresh
+ */
+export function clearETagCache(): void {
+ etagCache.clear();
+ dataCache.clear();
+ console.debug("[ETag] Cache cleared");
+}
+
+/**
+ * Invalidate specific endpoint cache
+ * Useful after mutations that affect specific resources
+ */
+export function invalidateETagCache(endpoint: string, method = "GET"): void {
+ const cleanEndpoint = endpoint.startsWith("/api") ? endpoint.substring(4) : endpoint;
+ const fullUrl = `${API_BASE_URL}${cleanEndpoint}`;
+ const cacheKey = `${method}:${fullUrl}`;
+
+ etagCache.delete(cacheKey);
+ dataCache.delete(cacheKey);
+ console.debug(`[ETag] Cache invalidated for ${cleanEndpoint}`);
+}
+
+/**
+ * Get cache statistics for debugging
+ */
+export function getETagCacheStats(): {
+ etagCount: number;
+ dataCacheSize: number;
+ endpoints: string[];
+} {
+ return {
+ etagCount: etagCache.size,
+ dataCacheSize: dataCache.size,
+ endpoints: Array.from(etagCache.keys()),
+ };
+}
diff --git a/archon-ui-main/src/features/projects/tasks/TasksTab.tsx b/archon-ui-main/src/features/projects/tasks/TasksTab.tsx
new file mode 100644
index 0000000000..4b0cbbcb7e
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/TasksTab.tsx
@@ -0,0 +1,322 @@
+import { LayoutGrid, Plus, Table } from "lucide-react";
+import { useCallback, useState } from "react";
+import { DndProvider } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
+import { DeleteConfirmModal } from "../../ui/components/DeleteConfirmModal";
+import { Button } from "../../ui/primitives";
+import { cn, glassmorphism } from "../../ui/primitives/styles";
+import { TaskEditModal } from "./components/TaskEditModal";
+import { useDeleteTask, useProjectTasks, useUpdateTask } from "./hooks";
+import type { Task } from "./types";
+import { getReorderTaskOrder, ORDER_INCREMENT, validateTaskOrder } from "./utils";
+import { BoardView, TableView } from "./views";
+
+interface TasksTabProps {
+ projectId: string;
+}
+
+export const TasksTab = ({ projectId }: TasksTabProps) => {
+ const [viewMode, setViewMode] = useState<"table" | "board">("board");
+ const [editingTask, setEditingTask] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [taskToDelete, setTaskToDelete] = useState(null);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+ // Fetch tasks using TanStack Query
+ const { data: tasks = [], isLoading: isLoadingTasks } = useProjectTasks(projectId);
+
+ // Mutations for task operations
+ const updateTaskMutation = useUpdateTask(projectId);
+ const deleteTaskMutation = useDeleteTask(projectId);
+
+ // Modal management functions
+ const openEditModal = (task: Task) => {
+ setEditingTask(task);
+ setIsModalOpen(true);
+ };
+
+ const openCreateModal = () => {
+ setEditingTask(null);
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setEditingTask(null);
+ setIsModalOpen(false);
+ };
+
+ // Delete modal management functions
+ const openDeleteModal = (task: Task) => {
+ setTaskToDelete(task);
+ setShowDeleteModal(true);
+ };
+
+ const closeDeleteModal = () => {
+ setTaskToDelete(null);
+ setShowDeleteModal(false);
+ };
+
+ const confirmDeleteTask = () => {
+ if (!taskToDelete) return;
+
+ deleteTaskMutation.mutate(taskToDelete.id, {
+ onSuccess: () => {
+ closeDeleteModal();
+ },
+ onError: (error) => {
+ console.error("Failed to delete task:", error);
+ },
+ });
+ };
+
+ // Get default order for new tasks in a status
+ const getDefaultTaskOrder = useCallback((statusTasks: Task[]) => {
+ if (statusTasks.length === 0) return ORDER_INCREMENT;
+ const maxOrder = Math.max(...statusTasks.map((t) => t.task_order));
+ return maxOrder + ORDER_INCREMENT;
+ }, []);
+
+ // Task reordering - immediate update
+ const handleTaskReorder = useCallback(
+ async (taskId: string, targetIndex: number, status: Task["status"]) => {
+ // Get all tasks in the target status, sorted by current order
+ const statusTasks = (tasks as Task[])
+ .filter((task) => task.status === status)
+ .sort((a, b) => a.task_order - b.task_order);
+
+ const movingTaskIndex = statusTasks.findIndex((task) => task.id === taskId);
+ if (movingTaskIndex === -1 || targetIndex < 0 || targetIndex > statusTasks.length) return;
+ if (movingTaskIndex === targetIndex) return;
+
+ // Calculate new position using battle-tested utility
+ const newPosition = getReorderTaskOrder(statusTasks, taskId, targetIndex);
+
+ // Update immediately with optimistic updates
+ try {
+ await updateTaskMutation.mutateAsync({
+ taskId,
+ updates: {
+ task_order: newPosition,
+ },
+ });
+ } catch (error) {
+ console.error("Failed to reorder task:", error, {
+ taskId,
+ newPosition,
+ });
+ // Error toast handled by mutation
+ }
+ },
+ [tasks, updateTaskMutation],
+ );
+
+ // Move task to different status
+ const moveTask = useCallback(
+ async (taskId: string, newStatus: Task["status"]) => {
+ const movingTask = (tasks as Task[]).find((task) => task.id === taskId);
+ if (!movingTask || movingTask.status === newStatus) return;
+
+ try {
+ // Calculate position for new status
+ const tasksInNewStatus = (tasks as Task[]).filter((t) => t.status === newStatus);
+ const newOrder = getDefaultTaskOrder(tasksInNewStatus);
+
+ // Update via mutation (handles optimistic updates)
+ await updateTaskMutation.mutateAsync({
+ taskId,
+ updates: {
+ status: newStatus,
+ task_order: newOrder,
+ },
+ });
+
+ // Success handled by mutation
+ } catch (error) {
+ console.error("Failed to move task:", error, { taskId, newStatus });
+ // Error toast handled by mutation
+ }
+ },
+ [tasks, updateTaskMutation, getDefaultTaskOrder],
+ );
+
+ const completeTask = useCallback(
+ (taskId: string) => {
+ moveTask(taskId, "done");
+ },
+ [moveTask],
+ );
+
+ // Inline update for task fields
+ const updateTaskInline = async (taskId: string, updates: Partial) => {
+ try {
+ // Validate task_order if present (ensures integer precision)
+ const processedUpdates = { ...updates };
+ if (processedUpdates.task_order !== undefined) {
+ processedUpdates.task_order = validateTaskOrder(processedUpdates.task_order);
+ }
+
+ await updateTaskMutation.mutateAsync({
+ taskId,
+ updates: processedUpdates,
+ });
+ } catch (error) {
+ console.error("Failed to update task:", error, { taskId, updates });
+ // Error toast handled by mutation
+ }
+ };
+
+ if (isLoadingTasks) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {/* Main content - Table or Board view */}
+
+ {viewMode === "table" ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Fixed View Controls using Radix primitives */}
+
+
+ {/* Edit/Create Task Modal */}
+
+
+ {/* Delete Task Modal */}
+
+
+
+ );
+};
+
+// Extracted ViewControls component using Radix primitives
+interface ViewControlsProps {
+ viewMode: "table" | "board";
+ onViewChange: (mode: "table" | "board") => void;
+ onAddTask: () => void;
+}
+
+const ViewControls = ({ viewMode, onViewChange, onAddTask }: ViewControlsProps) => {
+ return (
+
+
+ {/* Add Task Button with Glassmorphism */}
+
+
+ Add Task
+ {/* Glow effect */}
+
+
+
+ {/* View Toggle Controls with Glassmorphism */}
+
+
onViewChange("table")}
+ className={cn(
+ "px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300",
+ viewMode === "table"
+ ? "text-cyan-600 dark:text-cyan-400"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300",
+ )}
+ >
+
+ Table
+ {viewMode === "table" && (
+
+ )}
+
+
+
onViewChange("board")}
+ className={cn(
+ "px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300",
+ viewMode === "board"
+ ? "text-purple-600 dark:text-purple-400"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300",
+ )}
+ >
+
+ Board
+ {viewMode === "board" && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/EditableTableCell.tsx b/archon-ui-main/src/features/projects/tasks/components/EditableTableCell.tsx
new file mode 100644
index 0000000000..cde92f74fe
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/EditableTableCell.tsx
@@ -0,0 +1,170 @@
+import type React from "react";
+import { useEffect, useRef, useState } from "react";
+import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../ui/primitives";
+import { cn } from "../../../ui/primitives/styles";
+
+interface EditableTableCellProps {
+ value: string;
+ onSave: (value: string) => Promise;
+ type?: "text" | "select" | "status" | "assignee";
+ options?: readonly string[];
+ placeholder?: string;
+ className?: string;
+ isUpdating?: boolean;
+}
+
+// Status options for the status select
+const STATUS_OPTIONS = ["todo", "doing", "review", "done"] as const;
+
+// Assignee options
+const ASSIGNEE_OPTIONS = ["User", "Archon", "AI IDE Agent"] as const;
+
+export const EditableTableCell = ({
+ value,
+ onSave,
+ type = "text",
+ options,
+ placeholder = "Click to edit",
+ className,
+ isUpdating = false,
+}: EditableTableCellProps) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editValue, setEditValue] = useState(value);
+ const [isSaving, setIsSaving] = useState(false);
+ const inputRef = useRef(null);
+
+ // Update edit value when prop changes
+ useEffect(() => {
+ setEditValue(value);
+ }, [value]);
+
+ // Focus input when editing starts
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ const handleSave = async () => {
+ if (editValue === value) {
+ setIsEditing(false);
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ await onSave(editValue);
+ setIsEditing(false);
+ } catch (error) {
+ console.error("Failed to save:", error);
+ // Reset on error
+ setEditValue(value);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setEditValue(value);
+ setIsEditing(false);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ handleCancel();
+ }
+ };
+
+ // Get the appropriate options based on type
+ const selectOptions = type === "status" ? STATUS_OPTIONS : type === "assignee" ? ASSIGNEE_OPTIONS : options || [];
+
+ if (!isEditing) {
+ return (
+ // biome-ignore lint/a11y/useSemanticElements: Table cell transforms into input on click - can't use semantic button
+ !isUpdating && setIsEditing(true)}
+ onKeyDown={(e) => {
+ if ((e.key === "Enter" || e.key === " ") && !isUpdating) {
+ e.preventDefault();
+ setIsEditing(true);
+ }
+ }}
+ className={cn(
+ "cursor-pointer px-2 py-1 min-h-[28px]",
+ "hover:bg-gray-100/50 dark:hover:bg-gray-800/50",
+ "rounded transition-colors",
+ "flex items-center",
+ isUpdating && "opacity-50 cursor-not-allowed",
+ className,
+ )}
+ title={value || placeholder}
+ >
+ {value || placeholder}
+
+ );
+ }
+
+ // Render select for select types
+ if (type === "select" || type === "status" || type === "assignee") {
+ return (
+ {
+ setEditValue(newValue);
+ // Auto-save on select
+ setTimeout(() => {
+ setEditValue(newValue);
+ onSave(newValue);
+ setIsEditing(false);
+ }, 0);
+ }}
+ disabled={isSaving}
+ >
+
+
+
+
+ {selectOptions.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ );
+ }
+
+ // Render input for text type
+ return (
+ setEditValue(e.target.value)}
+ onBlur={handleSave}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ disabled={isSaving}
+ className={cn(
+ "h-7 text-sm",
+ "border-cyan-400 dark:border-cyan-600",
+ "focus:ring-1 focus:ring-cyan-400",
+ className,
+ )}
+ />
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/FeatureSelect.tsx b/archon-ui-main/src/features/projects/tasks/components/FeatureSelect.tsx
new file mode 100644
index 0000000000..5eb794ec5a
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/FeatureSelect.tsx
@@ -0,0 +1,61 @@
+/**
+ * FeatureSelect Component
+ *
+ * Radix-based feature selection with autocomplete
+ * Replaces the legacy FeatureInput component
+ */
+
+import React, { memo } from "react";
+import { ComboBox, type ComboBoxOption } from "../../../ui/primitives";
+
+interface FeatureSelectProps {
+ value: string;
+ onChange: (value: string) => void;
+ projectFeatures: Array<{
+ id: string;
+ label: string;
+ type?: string;
+ color?: string;
+ }>;
+ isLoadingFeatures?: boolean;
+ placeholder?: string;
+ className?: string;
+}
+
+export const FeatureSelect = memo(
+ ({
+ value,
+ onChange,
+ projectFeatures,
+ isLoadingFeatures = false,
+ placeholder = "Select or create feature...",
+ className,
+ }: FeatureSelectProps) => {
+ // Transform features to ComboBox options
+ const options: ComboBoxOption[] = React.useMemo(
+ () =>
+ projectFeatures.map((feature) => ({
+ value: feature.label,
+ label: feature.label,
+ description: feature.type ? `Type: ${feature.type}` : undefined,
+ })),
+ [projectFeatures],
+ );
+
+ return (
+
+ );
+ },
+);
+
+FeatureSelect.displayName = "FeatureSelect";
diff --git a/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx b/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx
new file mode 100644
index 0000000000..c1edb2d8b8
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx
@@ -0,0 +1,103 @@
+import { useRef } from "react";
+import { useDrop } from "react-dnd";
+import { cn } from "../../../ui/primitives/styles";
+import type { Task } from "../types";
+import { getColumnColor, getColumnGlow, ItemTypes } from "../utils/task-styles";
+import { TaskCard } from "./TaskCard";
+
+interface KanbanColumnProps {
+ status: Task["status"];
+ title: string;
+ tasks: Task[];
+ projectId: string;
+ onTaskMove: (taskId: string, newStatus: Task["status"]) => void;
+ onTaskReorder: (taskId: string, targetIndex: number, status: Task["status"]) => void;
+ onTaskEdit?: (task: Task) => void;
+ onTaskDelete?: (task: Task) => void;
+ hoveredTaskId: string | null;
+ onTaskHover: (taskId: string | null) => void;
+}
+
+export const KanbanColumn = ({
+ status,
+ title,
+ tasks,
+ projectId,
+ onTaskMove,
+ onTaskReorder,
+ onTaskEdit,
+ onTaskDelete,
+ hoveredTaskId,
+ onTaskHover,
+}: KanbanColumnProps) => {
+ const ref = useRef(null);
+
+ const [{ isOver }, drop] = useDrop({
+ accept: ItemTypes.TASK,
+ drop: (item: { id: string; status: Task["status"] }) => {
+ if (item.status !== status) {
+ onTaskMove(item.id, status);
+ }
+ },
+ collect: (monitor) => ({
+ isOver: !!monitor.isOver(),
+ }),
+ });
+
+ drop(ref);
+
+ return (
+
+ {/* Column Header with Glassmorphism */}
+
+
{title}
+ {/* Column header glow effect */}
+
+
+
+ {/* Tasks Container */}
+
+ {tasks.length === 0 ? (
+
No tasks
+ ) : (
+ tasks.map((task, index) => (
+
+ ))
+ )}
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskAssignee.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskAssignee.tsx
new file mode 100644
index 0000000000..d1910ab08b
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/TaskAssignee.tsx
@@ -0,0 +1,141 @@
+import { Bot, User } from "lucide-react";
+import type React from "react";
+import { Select, SelectContent, SelectItem, SelectTrigger } from "../../../ui/primitives";
+import { cn } from "../../../ui/primitives/styles";
+import type { Assignee } from "../types";
+
+interface TaskAssigneeProps {
+ assignee: Assignee;
+ onAssigneeChange?: (newAssignee: Assignee) => void;
+ isLoading?: boolean;
+}
+
+const ASSIGNEE_OPTIONS: Assignee[] = ["User", "Archon", "AI IDE Agent"];
+
+// Get icon for each assignee type
+const getAssigneeIcon = (assigneeName: Assignee, size: "sm" | "md" = "sm") => {
+ const sizeClass = size === "sm" ? "w-3 h-3" : "w-4 h-4";
+
+ switch (assigneeName) {
+ case "User":
+ return ;
+ case "AI IDE Agent":
+ return ;
+ case "Archon":
+ return ;
+ default:
+ return ;
+ }
+};
+
+// Get glow effect for each assignee type
+const getAssigneeStyles = (assigneeName: Assignee) => {
+ switch (assigneeName) {
+ case "User":
+ return {
+ glow: "shadow-[0_0_10px_rgba(59,130,246,0.4)]",
+ hoverGlow: "hover:shadow-[0_0_12px_rgba(59,130,246,0.5)]",
+ color: "text-blue-600 dark:text-blue-400",
+ };
+ case "AI IDE Agent":
+ return {
+ glow: "shadow-[0_0_10px_rgba(168,85,247,0.4)]",
+ hoverGlow: "hover:shadow-[0_0_12px_rgba(168,85,247,0.5)]",
+ color: "text-purple-600 dark:text-purple-400",
+ };
+ case "Archon":
+ return {
+ glow: "shadow-[0_0_10px_rgba(34,211,238,0.4)]",
+ hoverGlow: "hover:shadow-[0_0_12px_rgba(34,211,238,0.5)]",
+ color: "text-cyan-600 dark:text-cyan-400",
+ };
+ default:
+ return {
+ glow: "shadow-[0_0_10px_rgba(59,130,246,0.4)]",
+ hoverGlow: "hover:shadow-[0_0_12px_rgba(59,130,246,0.5)]",
+ color: "text-blue-600 dark:text-blue-400",
+ };
+ }
+};
+
+export const TaskAssignee: React.FC = ({ assignee, onAssigneeChange, isLoading = false }) => {
+ const styles = getAssigneeStyles(assignee);
+
+ // If no change handler, just show a static display
+ if (!onAssigneeChange) {
+ return (
+
+
+ {getAssigneeIcon(assignee, "md")}
+
+
{assignee}
+
+ );
+ }
+
+ return (
+ onAssigneeChange(value as Assignee)}>
+
+
+
+ {getAssigneeIcon(assignee, "md")}
+
+
{assignee}
+
+
+
+
+ {ASSIGNEE_OPTIONS.map((option) => {
+ const optionStyles = getAssigneeStyles(option);
+
+ return (
+
+
+
+ {getAssigneeIcon(option, "md")}
+
+
{option}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx
new file mode 100644
index 0000000000..48aa87f2e3
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx
@@ -0,0 +1,228 @@
+import { Tag } from "lucide-react";
+import type React from "react";
+import { useCallback, useState } from "react";
+import { useDrag, useDrop } from "react-dnd";
+import { useTaskActions } from "../hooks";
+import type { Assignee, Task } from "../types";
+import { getOrderColor, getOrderGlow, ItemTypes } from "../utils/task-styles";
+import { TaskAssignee } from "./TaskAssignee";
+import { TaskCardActions } from "./TaskCardActions";
+import { type Priority, TaskPriority } from "./TaskPriority";
+
+export interface TaskCardProps {
+ task: Task;
+ index: number;
+ projectId: string; // Need this for mutations
+ onTaskReorder: (taskId: string, targetIndex: number, status: Task["status"]) => void;
+ onEdit?: (task: Task) => void; // Optional edit handler
+ onDelete?: (task: Task) => void; // Optional delete handler
+ hoveredTaskId?: string | null;
+ onTaskHover?: (taskId: string | null) => void;
+ selectedTasks?: Set;
+ onTaskSelect?: (taskId: string) => void;
+}
+
+export const TaskCard: React.FC = ({
+ task,
+ index,
+ projectId,
+ onTaskReorder,
+ onEdit,
+ onDelete,
+ hoveredTaskId,
+ onTaskHover,
+ selectedTasks,
+ onTaskSelect,
+}) => {
+ // Local state for frontend-only priority
+ // NOTE: Priority is display-only and doesn't sync with backend yet
+ const [localPriority, setLocalPriority] = useState("medium");
+
+ // Use business logic hook
+ const { changeAssignee, isUpdating } = useTaskActions(projectId);
+
+ // Handlers - now just call hook methods
+ const handleEdit = useCallback(() => {
+ // Call the onEdit prop if provided, otherwise log
+ if (onEdit) {
+ onEdit(task);
+ } else {
+ // Edit task - no handler provided
+ }
+ }, [onEdit, task]);
+
+ const handleDelete = useCallback(() => {
+ if (onDelete) {
+ onDelete(task);
+ } else {
+ // Delete task - no handler provided
+ }
+ }, [onDelete, task]);
+
+ const handlePriorityChange = useCallback((priority: Priority) => {
+ // Frontend-only priority change
+ setLocalPriority(priority);
+ }, []);
+
+ const handleAssigneeChange = useCallback(
+ (newAssignee: Assignee) => {
+ changeAssignee(task.id, newAssignee);
+ },
+ [changeAssignee, task.id],
+ );
+
+ 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;
+
+ // Move the task immediately for visual feedback
+ onTaskReorder(draggedItem.id, hoveredIndex, task.status);
+
+ // Update the dragged item's index to prevent re-triggering
+ draggedItem.index = hoveredIndex;
+ },
+ });
+
+ const isHighlighted = hoveredTaskId === task.id;
+ const isSelected = selectedTasks?.has(task.id) || false;
+
+ const handleMouseEnter = () => {
+ onTaskHover?.(task.id);
+ };
+
+ const handleMouseLeave = () => {
+ onTaskHover?.(null);
+ };
+
+ const handleTaskClick = (e: React.MouseEvent) => {
+ if (e.ctrlKey || e.metaKey) {
+ e.stopPropagation();
+ onTaskSelect?.(task.id);
+ }
+ };
+
+ // Glassmorphism styling constants
+ 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 backdrop-blur-md";
+ const transitionStyles = "transition-all duration-200 ease-in-out";
+
+ // Subtle highlight effect for related tasks
+ const highlightGlow = isHighlighted ? "border-cyan-400/50 shadow-[0_0_8px_rgba(34,211,238,0.2)]" : "";
+
+ // Selection styling with glassmorphism
+ const selectionGlow = isSelected
+ ? "border-blue-500 shadow-[0_0_12px_rgba(59,130,246,0.4)] bg-blue-50/30 dark:bg-blue-900/20"
+ : "";
+
+ // Beautiful hover effect with glowing borders
+ 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)]";
+
+ return (
+ // biome-ignore lint/a11y/useSemanticElements: Drag-and-drop card with react-dnd - requires div for drag handle
+ drag(drop(node))}
+ role="button"
+ tabIndex={0}
+ className={`w-full min-h-[140px] cursor-move relative ${isDragging ? "opacity-50 scale-90" : "scale-100 opacity-100"} ${transitionStyles} group`}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ onClick={handleTaskClick}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ if (onEdit) {
+ onEdit(task);
+ }
+ }
+ }}
+ >
+
+ {/* Priority indicator with beautiful glow */}
+
+
+ {/* Content container with fixed padding */}
+
+ {/* Header with feature and actions */}
+
+ {task.feature && (
+
+
+ {task.feature}
+
+ )}
+
+ {/* Action buttons group */}
+
+
+
+
+
+ {/* Title */}
+
+ {task.title}
+
+
+ {/* Description - visible when task has description */}
+ {task.description && (
+
+
+ {task.description}
+
+
+ )}
+
+ {/* Spacer when no description */}
+ {!task.description &&
}
+
+ {/* Footer with assignee - glassmorphism styling */}
+
+
+
+ {/* Priority display (frontend-only for now) */}
+
+
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskCardActions.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskCardActions.tsx
new file mode 100644
index 0000000000..3070d521f2
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/TaskCardActions.tsx
@@ -0,0 +1,112 @@
+import { Clipboard, Edit, Trash2 } from "lucide-react";
+import type React from "react";
+import { useToast } from "../../../ui/hooks/useToast";
+import { cn, glassmorphism } from "../../../ui/primitives/styles";
+import { SimpleTooltip } from "../../../ui/primitives/tooltip";
+
+interface TaskCardActionsProps {
+ taskId: string;
+ taskTitle: string;
+ onEdit: () => void;
+ onDelete: () => void;
+ isDeleting?: boolean;
+}
+
+export const TaskCardActions: React.FC = ({
+ taskId,
+ taskTitle,
+ onEdit,
+ onDelete,
+ isDeleting = false,
+}) => {
+ const { showToast } = useToast();
+
+ const handleCopyId = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ try {
+ await navigator.clipboard.writeText(taskId);
+ showToast("Task ID copied to clipboard", "success");
+ } catch {
+ // Fallback for older browsers
+ try {
+ const ta = document.createElement("textarea");
+ ta.value = taskId;
+ ta.style.position = "fixed";
+ ta.style.opacity = "0";
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy");
+ document.body.removeChild(ta);
+ showToast("Task ID copied to clipboard", "success");
+ } catch {
+ showToast("Failed to copy Task ID", "error");
+ }
+ }
+ };
+
+ return (
+
+
+ {
+ e.stopPropagation();
+ if (!isDeleting) onDelete();
+ }}
+ disabled={isDeleting}
+ className={cn(
+ "w-5 h-5 rounded-full flex items-center justify-center",
+ "transition-all duration-300",
+ glassmorphism.priority.critical.background,
+ glassmorphism.priority.critical.text,
+ glassmorphism.priority.critical.hover,
+ glassmorphism.priority.critical.glow,
+ isDeleting && "opacity-50 cursor-not-allowed",
+ )}
+ aria-label={isDeleting ? "Deleting task..." : `Delete ${taskTitle}`}
+ >
+
+
+
+
+
+ {
+ e.stopPropagation();
+ onEdit();
+ }}
+ className={cn(
+ "w-5 h-5 rounded-full flex items-center justify-center",
+ "transition-all duration-300",
+ "bg-cyan-100/80 dark:bg-cyan-500/20",
+ "text-cyan-600 dark:text-cyan-400",
+ "hover:bg-cyan-200 dark:hover:bg-cyan-500/30",
+ "hover:shadow-[0_0_10px_rgba(34,211,238,0.3)]",
+ )}
+ aria-label={`Edit ${taskTitle}`}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskEditModal.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskEditModal.tsx
new file mode 100644
index 0000000000..7b40b51035
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/TaskEditModal.tsx
@@ -0,0 +1,209 @@
+import { memo, useCallback, useEffect, useState } from "react";
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ FormField,
+ FormGrid,
+ Input,
+ Label,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ TextArea,
+} from "../../../ui/primitives";
+import { useTaskEditor } from "../hooks";
+import type { Assignee, Task } from "../types";
+import { FeatureSelect } from "./FeatureSelect";
+import type { Priority } from "./TaskPriority";
+
+interface TaskEditModalProps {
+ isModalOpen: boolean;
+ editingTask: Task | null;
+ projectId: string;
+ onClose: () => void;
+ onSaved?: () => void;
+ onOpenChange?: (open: boolean) => void;
+}
+
+const ASSIGNEE_OPTIONS = ["User", "Archon", "AI IDE Agent"] as const;
+
+export const TaskEditModal = memo(
+ ({ isModalOpen, editingTask, projectId, onClose, onSaved, onOpenChange }: TaskEditModalProps) => {
+ const [localTask, setLocalTask] = useState | null>(null);
+
+ // Use business logic hook
+ const { projectFeatures, saveTask, isLoadingFeatures, isSaving: isSavingTask } = useTaskEditor(projectId);
+
+ // Sync local state with editingTask when it changes
+ useEffect(() => {
+ if (editingTask) {
+ setLocalTask(editingTask);
+ } else {
+ // Reset for new task
+ setLocalTask({
+ title: "",
+ description: "",
+ status: "todo",
+ assignee: "User" as Assignee,
+ feature: "",
+ priority: "medium" as Priority, // Frontend-only priority
+ });
+ }
+ }, [editingTask]);
+
+ // Memoized handlers for input changes
+ const handleTitleChange = useCallback((value: string) => {
+ setLocalTask((prev) => (prev ? { ...prev, title: value } : null));
+ }, []);
+
+ const handleDescriptionChange = useCallback((value: string) => {
+ setLocalTask((prev) => (prev ? { ...prev, description: value } : null));
+ }, []);
+
+ const handleFeatureChange = useCallback((value: string) => {
+ setLocalTask((prev) => (prev ? { ...prev, feature: value } : null));
+ }, []);
+
+ const handleSave = useCallback(() => {
+ // All validation is now in the hook
+ saveTask(localTask, editingTask, () => {
+ onSaved?.();
+ onClose();
+ });
+ }, [localTask, editingTask, saveTask, onSaved, onClose]);
+
+ const handleClose = useCallback(() => {
+ onClose();
+ }, [onClose]);
+
+ return (
+ !open && onClose())}>
+
+
+ {editingTask?.id ? "Edit Task" : "New Task"}
+
+
+
+
+ Title
+ handleTitleChange(e.target.value)}
+ placeholder="Enter task title"
+ />
+
+
+
+ Description
+ handleDescriptionChange(e.target.value)}
+ rows={5}
+ placeholder="Enter task description"
+ />
+
+
+
+
+ Status
+
+ setLocalTask((prev) => (prev ? { ...prev, status: value as Task["status"] } : null))
+ }
+ >
+
+
+
+
+ Todo
+ Doing
+ Review
+ Done
+
+
+
+
+
+ Priority
+
+ setLocalTask((prev) => (prev ? { ...prev, priority: value as Priority } : null))
+ }
+ >
+
+
+
+
+ Critical
+ High
+ Medium
+ Low
+
+
+
+
+
+
+
+ Assignee
+
+ setLocalTask((prev) => (prev ? { ...prev, assignee: value as Assignee } : null))
+ }
+ >
+
+
+
+
+ {ASSIGNEE_OPTIONS.map((option) => (
+
+ {option}
+
+ ))}
+
+
+
+
+
+ Feature
+
+
+
+
+
+
+
+ Cancel
+
+
+ {editingTask?.id ? "Update Task" : "Create Task"}
+
+
+
+
+ );
+ },
+);
+
+TaskEditModal.displayName = "TaskEditModal";
diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskPriority.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskPriority.tsx
new file mode 100644
index 0000000000..3e59bafb0a
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/TaskPriority.tsx
@@ -0,0 +1,143 @@
+/**
+ * TaskPriority Component
+ *
+ * Display-only priority selector for tasks.
+ * NOTE: Priority is currently frontend-only and doesn't affect task ordering.
+ * Task ordering is handled separately via drag-and-drop with task_order field.
+ * This is purely for visual categorization until backend priority support is added.
+ */
+
+import { AlertCircle } from "lucide-react";
+import type React from "react";
+import { Select, SelectContent, SelectItem, SelectTrigger } from "../../../ui/primitives/select";
+import { cn, glassmorphism } from "../../../ui/primitives/styles";
+
+export type Priority = "critical" | "high" | "medium" | "low";
+
+interface TaskPriorityProps {
+ priority?: Priority;
+ onPriorityChange?: (priority: Priority) => void;
+ isLoading?: boolean;
+}
+
+// Priority options for the dropdown
+const PRIORITY_OPTIONS: Array<{
+ value: Priority;
+ label: string;
+ color: string;
+}> = [
+ { value: "critical", label: "Critical", color: "text-red-600" },
+ { value: "high", label: "High", color: "text-orange-600" },
+ { value: "medium", label: "Medium", color: "text-blue-600" },
+ { value: "low", label: "Low", color: "text-gray-600" },
+];
+
+export const TaskPriority: React.FC = ({
+ priority = "medium",
+ onPriorityChange,
+ isLoading = false,
+}) => {
+ // Get priority-specific styling with Tron glow
+ const getPriorityStyles = (priorityValue: Priority) => {
+ switch (priorityValue) {
+ case "critical":
+ return {
+ background: glassmorphism.priority.critical.background,
+ text: glassmorphism.priority.critical.text,
+ hover: glassmorphism.priority.critical.hover,
+ glow: glassmorphism.priority.critical.glow,
+ iconColor: "text-red-500",
+ };
+ case "high":
+ return {
+ background: glassmorphism.priority.high.background,
+ text: glassmorphism.priority.high.text,
+ hover: glassmorphism.priority.high.hover,
+ glow: glassmorphism.priority.high.glow,
+ iconColor: "text-orange-500",
+ };
+ case "medium":
+ return {
+ background: glassmorphism.priority.medium.background,
+ text: glassmorphism.priority.medium.text,
+ hover: glassmorphism.priority.medium.hover,
+ glow: glassmorphism.priority.medium.glow,
+ iconColor: "text-blue-500",
+ };
+ default:
+ return {
+ background: glassmorphism.priority.low.background,
+ text: glassmorphism.priority.low.text,
+ hover: glassmorphism.priority.low.hover,
+ glow: glassmorphism.priority.low.glow,
+ iconColor: "text-gray-500",
+ };
+ }
+ };
+
+ const currentStyles = getPriorityStyles(priority);
+ const currentOption = PRIORITY_OPTIONS.find((opt) => opt.value === priority) || PRIORITY_OPTIONS[2]; // Default to medium
+
+ // If no change handler, just show a static button
+ if (!onPriorityChange) {
+ return (
+
+
+ {currentOption.label}
+
+ );
+ }
+
+ return (
+ onPriorityChange(value as Priority)}>
+
+
+
+
{currentOption.label}
+
+
+
+
+ {PRIORITY_OPTIONS.map((option) => {
+ const optionStyles = getPriorityStyles(option.value);
+
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/components/index.ts b/archon-ui-main/src/features/projects/tasks/components/index.ts
new file mode 100644
index 0000000000..6e6c9e734d
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/components/index.ts
@@ -0,0 +1,16 @@
+/**
+ * Task Management Components
+ *
+ * Simplified and refactored task components following vertical slice architecture.
+ * Removed complex flip animations and over-engineering for better maintainability.
+ */
+
+export { EditableTableCell } from "./EditableTableCell";
+export { FeatureSelect } from "./FeatureSelect";
+export { KanbanColumn } from "./KanbanColumn";
+export { TaskAssignee } from "./TaskAssignee";
+export type { TaskCardProps } from "./TaskCard";
+export { TaskCard } from "./TaskCard";
+export { TaskCardActions } from "./TaskCardActions";
+export { TaskEditModal } from "./TaskEditModal";
+export { TaskPriority as TaskPriorityComponent } from "./TaskPriority";
diff --git a/archon-ui-main/src/features/projects/tasks/hooks/index.ts b/archon-ui-main/src/features/projects/tasks/hooks/index.ts
new file mode 100644
index 0000000000..9dc2cdc1eb
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/hooks/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Task Hooks
+ *
+ * Business logic hooks for task management operations.
+ * These hooks encapsulate the business logic that should NOT live in components.
+ */
+
+// Business logic hooks
+export { useTaskActions } from "./useTaskActions";
+export { useTaskEditor } from "./useTaskEditor";
+
+// TanStack Query hooks
+export {
+ taskKeys,
+ useCreateTask,
+ useDeleteTask,
+ useProjectTasks,
+ useUpdateTask,
+} from "./useTaskQueries";
diff --git a/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts b/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts
new file mode 100644
index 0000000000..a92829875c
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts
@@ -0,0 +1,195 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { taskKeys, useProjectTasks, useCreateTask } from '../useTaskQueries';
+import type { Task } from '../../types';
+import React from 'react';
+
+// Mock the services
+vi.mock('../../services', () => ({
+ taskService: {
+ getTasksByProject: vi.fn(),
+ createTask: vi.fn(),
+ updateTask: vi.fn(),
+ deleteTask: vi.fn(),
+ },
+}));
+
+// Mock the toast hook
+vi.mock('../../../../ui/hooks/useToast', () => ({
+ useToast: () => ({
+ showToast: vi.fn(),
+ }),
+}));
+
+// Mock smart polling
+vi.mock('../../../../ui/hooks', () => ({
+ useSmartPolling: () => ({
+ refetchInterval: 5000,
+ isPaused: false,
+ }),
+}));
+
+// Test wrapper with QueryClient
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ return ({ children }: { children: React.ReactNode }) =>
+ React.createElement(QueryClientProvider, { client: queryClient }, children);
+};
+
+describe('useTaskQueries', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('taskKeys', () => {
+ it('should generate correct query keys', () => {
+ expect(taskKeys.all('project-123')).toEqual(['projects', 'project-123', 'tasks']);
+ });
+ });
+
+ describe('useProjectTasks', () => {
+ it('should fetch tasks for a project', async () => {
+ const mockTasks: Task[] = [
+ {
+ id: 'task-1',
+ project_id: 'project-123',
+ title: 'Test Task',
+ description: 'Test Description',
+ status: 'todo',
+ assignee: 'User',
+ task_order: 100,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ },
+ ];
+
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.getTasksByProject).mockResolvedValue(mockTasks);
+
+ const { result } = renderHook(() => useProjectTasks('project-123'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockTasks);
+ });
+
+ expect(taskService.getTasksByProject).toHaveBeenCalledWith('project-123');
+ });
+
+ it('should not fetch tasks when projectId is undefined', () => {
+ const { result } = renderHook(() => useProjectTasks(undefined), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.isFetching).toBe(false);
+ expect(result.current.data).toBeUndefined();
+ });
+
+ it('should respect enabled flag', () => {
+ const { result } = renderHook(() => useProjectTasks('project-123', false), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.isFetching).toBe(false);
+ expect(result.current.data).toBeUndefined();
+ });
+ });
+
+ describe('useCreateTask', () => {
+ it('should optimistically add task and replace with server response', async () => {
+ const newTask: Task = {
+ id: 'real-task-id',
+ project_id: 'project-123',
+ title: 'New Task',
+ description: 'New Description',
+ status: 'todo',
+ assignee: 'User',
+ task_order: 100,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ };
+
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.createTask).mockResolvedValue(newTask);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateTask(), { wrapper });
+
+ await result.current.mutateAsync({
+ project_id: 'project-123',
+ title: 'New Task',
+ description: 'New Description',
+ status: 'todo',
+ assignee: 'User',
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(taskService.createTask).toHaveBeenCalledWith({
+ project_id: 'project-123',
+ title: 'New Task',
+ description: 'New Description',
+ status: 'todo',
+ assignee: 'User',
+ });
+ });
+ });
+
+ it('should provide default values for optional fields', async () => {
+ const newTask: Task = {
+ id: 'real-task-id',
+ project_id: 'project-123',
+ title: 'Minimal Task',
+ description: '',
+ status: 'todo',
+ assignee: 'User',
+ task_order: 100,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ };
+
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.createTask).mockResolvedValue(newTask);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateTask(), { wrapper });
+
+ await result.current.mutateAsync({
+ project_id: 'project-123',
+ title: 'Minimal Task',
+ description: '',
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+ });
+
+ it('should rollback on error', async () => {
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.createTask).mockRejectedValue(new Error('Network error'));
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateTask(), { wrapper });
+
+ await expect(
+ result.current.mutateAsync({
+ project_id: 'project-123',
+ title: 'Failed Task',
+ description: 'This will fail',
+ })
+ ).rejects.toThrow('Network error');
+ });
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/projects/tasks/hooks/useTaskActions.ts b/archon-ui-main/src/features/projects/tasks/hooks/useTaskActions.ts
new file mode 100644
index 0000000000..b74d207d53
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/hooks/useTaskActions.ts
@@ -0,0 +1,69 @@
+import { useCallback, useState } from "react";
+import type { Assignee, Task, UseTaskActionsReturn } from "../types";
+import { useDeleteTask, useUpdateTask } from "./useTaskQueries";
+
+export const useTaskActions = (projectId: string): UseTaskActionsReturn => {
+ const updateTaskMutation = useUpdateTask(projectId);
+ const deleteTaskMutation = useDeleteTask(projectId);
+
+ // Delete confirmation state - store full task object for proper modal display
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [taskToDelete, setTaskToDelete] = useState(null);
+
+ // Assignee change handler
+ const changeAssignee = useCallback(
+ (taskId: string, newAssignee: string) => {
+ updateTaskMutation.mutate({
+ taskId,
+ updates: { assignee: newAssignee as Assignee },
+ });
+ },
+ [updateTaskMutation],
+ );
+
+ // Delete task handler with confirmation flow - now accepts full task object
+ const initiateDelete = useCallback((task: Task) => {
+ setTaskToDelete(task);
+ setShowDeleteConfirm(true);
+ }, []);
+
+ // Confirm and execute deletion
+ const confirmDelete = useCallback(() => {
+ if (!taskToDelete) return;
+
+ deleteTaskMutation.mutate(taskToDelete.id, {
+ onSuccess: () => {
+ // Success toast handled by mutation
+ setShowDeleteConfirm(false);
+ setTaskToDelete(null);
+ },
+ onError: (error) => {
+ console.error("Failed to delete task:", error, { taskToDelete });
+ // Error toast handled by mutation
+ // Modal stays open on error so user can retry
+ },
+ });
+ }, [deleteTaskMutation, taskToDelete]);
+
+ // Cancel deletion
+ const cancelDelete = useCallback(() => {
+ setShowDeleteConfirm(false);
+ setTaskToDelete(null);
+ }, []);
+
+ return {
+ // Actions
+ changeAssignee,
+ initiateDelete,
+ confirmDelete,
+ cancelDelete,
+
+ // State
+ showDeleteConfirm,
+ taskToDelete,
+
+ // Loading states
+ isUpdating: updateTaskMutation.isPending,
+ isDeleting: deleteTaskMutation.isPending,
+ };
+};
diff --git a/archon-ui-main/src/features/projects/tasks/hooks/useTaskEditor.ts b/archon-ui-main/src/features/projects/tasks/hooks/useTaskEditor.ts
new file mode 100644
index 0000000000..d0c87486a8
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/hooks/useTaskEditor.ts
@@ -0,0 +1,125 @@
+import { useCallback } from "react";
+import { useToast } from "../../../ui/hooks/useToast";
+import { useProjectFeatures } from "../../hooks/useProjectQueries";
+import type { Assignee, CreateTaskRequest, Task, UpdateTaskRequest, UseTaskEditorReturn } from "../types";
+import { useCreateTask, useUpdateTask } from "./useTaskQueries";
+
+export const useTaskEditor = (projectId: string): UseTaskEditorReturn => {
+ const { showToast } = useToast();
+
+ // TanStack Query hooks
+ const { data: featuresData, isLoading: isLoadingFeatures } = useProjectFeatures(projectId);
+ const createTaskMutation = useCreateTask();
+ const updateTaskMutation = useUpdateTask(projectId);
+
+ // Transform features data
+ const projectFeatures = (featuresData?.features || []) as Array<{
+ id: string;
+ label: string;
+ type?: string;
+ color?: string;
+ }>;
+ const isSaving = createTaskMutation.isPending || updateTaskMutation.isPending;
+
+ // Get default order for new tasks based on status
+ const getDefaultTaskOrder = useCallback((status: Task["status"]) => {
+ // Simple priority mapping: todo=50, doing=25, review=75, done=100
+ const statusOrderMap = { todo: 50, doing: 25, review: 75, done: 100 };
+ return statusOrderMap[status] || 50;
+ }, []);
+
+ // Build update object with only changed fields
+ const buildTaskUpdates = useCallback((localTask: Partial, editingTask: Task) => {
+ const updates: UpdateTaskRequest = {};
+
+ if (localTask.title !== editingTask.title) updates.title = localTask.title;
+ if (localTask.description !== editingTask.description) updates.description = localTask.description;
+ if (localTask.status !== editingTask.status) updates.status = localTask.status;
+ if (localTask.assignee !== editingTask.assignee) updates.assignee = localTask.assignee || "User";
+ if (localTask.task_order !== editingTask.task_order) updates.task_order = localTask.task_order;
+ if (localTask.feature !== editingTask.feature) updates.feature = localTask.feature || "";
+
+ return updates;
+ }, []);
+
+ // Build create request object
+ const buildCreateRequest = useCallback(
+ (localTask: Partial): CreateTaskRequest => {
+ return {
+ project_id: projectId,
+ title: localTask.title || "",
+ description: localTask.description || "",
+ status: (localTask.status as Task["status"]) || "todo",
+ assignee: (localTask.assignee as Assignee) || "User",
+ feature: localTask.feature || "",
+ task_order: localTask.task_order || getDefaultTaskOrder((localTask.status as Task["status"]) || "todo"),
+ };
+ },
+ [projectId, getDefaultTaskOrder],
+ );
+
+ // Save task (create or update) with full validation
+ const saveTask = useCallback(
+ async (localTask: Partial | null, editingTask: Task | null, onSuccess?: () => void) => {
+ // Validation moved here from component
+ if (!localTask) {
+ showToast("No task data provided", "error");
+ return;
+ }
+
+ if (!localTask.title?.trim()) {
+ showToast("Task title is required", "error");
+ return;
+ }
+
+ if (editingTask?.id) {
+ // Update existing task
+ const updates = buildTaskUpdates(localTask, editingTask);
+
+ updateTaskMutation.mutate(
+ {
+ taskId: editingTask.id,
+ updates,
+ },
+ {
+ onSuccess: () => {
+ // Success toast handled by mutation
+ onSuccess?.();
+ },
+ onError: (error) => {
+ console.error("Failed to update task:", error);
+ // Error toast handled by mutation
+ },
+ },
+ );
+ } else {
+ // Create new task
+ const newTaskData = buildCreateRequest(localTask);
+
+ createTaskMutation.mutate(newTaskData, {
+ onSuccess: () => {
+ // Success toast handled by mutation
+ onSuccess?.();
+ },
+ onError: (error) => {
+ console.error("Failed to create task:", error);
+ // Error toast handled by mutation
+ },
+ });
+ }
+ },
+ [buildTaskUpdates, buildCreateRequest, updateTaskMutation, createTaskMutation, showToast],
+ );
+
+ return {
+ // Data
+ projectFeatures,
+
+ // Actions
+ saveTask,
+
+ // Loading states
+ isLoadingFeatures,
+ isSaving,
+ };
+};
diff --git a/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts b/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts
new file mode 100644
index 0000000000..160c347b88
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts
@@ -0,0 +1,182 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useSmartPolling } from "../../../ui/hooks";
+import { useToast } from "../../../ui/hooks/useToast";
+import { projectKeys } from "../../hooks/useProjectQueries";
+import { taskService } from "../services";
+import type { CreateTaskRequest, Task, UpdateTaskRequest } from "../types";
+
+// Query keys factory for tasks
+export const taskKeys = {
+ all: (projectId: string) => ["projects", projectId, "tasks"] as const,
+};
+
+// Fetch tasks for a specific project
+export function useProjectTasks(projectId: string | undefined, enabled = true) {
+ const { refetchInterval } = useSmartPolling(5000); // 5 second base interval for faster MCP updates
+
+ return useQuery({
+ queryKey: projectId ? taskKeys.all(projectId) : ["tasks-undefined"],
+ queryFn: async () => {
+ if (!projectId) throw new Error("No project ID");
+ return taskService.getTasksByProject(projectId);
+ },
+ enabled: !!projectId && enabled,
+ refetchInterval, // Smart interval based on page visibility/focus
+ staleTime: 10000, // Consider data stale after 10 seconds
+ });
+}
+
+// Create task mutation with optimistic updates
+export function useCreateTask() {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+
+ return useMutation({
+ mutationFn: (taskData: CreateTaskRequest) => taskService.createTask(taskData),
+ onMutate: async (newTaskData) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: taskKeys.all(newTaskData.project_id) });
+
+ // Snapshot the previous value
+ const previousTasks = queryClient.getQueryData(taskKeys.all(newTaskData.project_id));
+
+ // Create optimistic task with temporary ID
+ const tempId = `temp-${Date.now()}`;
+ const optimisticTask: Task = {
+ id: tempId, // Temporary ID until real one comes back
+ ...newTaskData,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ // Ensure all required fields have defaults
+ task_order: newTaskData.task_order ?? 100,
+ status: newTaskData.status ?? "todo",
+ assignee: newTaskData.assignee ?? "User",
+ } as Task;
+
+ // Optimistically add the new task
+ queryClient.setQueryData(taskKeys.all(newTaskData.project_id), (old: Task[] | undefined) => {
+ if (!old) return [optimisticTask];
+ return [...old, optimisticTask];
+ });
+
+ return { previousTasks, tempId };
+ },
+ onError: (error, variables, context) => {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ console.error("Failed to create task:", error, { variables });
+ // Rollback on error
+ if (context?.previousTasks) {
+ queryClient.setQueryData(taskKeys.all(variables.project_id), context.previousTasks);
+ }
+ showToast(`Failed to create task: ${errorMessage}`, "error");
+ },
+ onSuccess: (data, variables, context) => {
+ // Replace optimistic task with real one from server
+ queryClient.setQueryData(taskKeys.all(variables.project_id), (old: Task[] | undefined) => {
+ if (!old) return [data];
+ // Replace only the specific temp task with real one
+ return old
+ .map((task) => (task.id === context?.tempId ? data : task))
+ .filter(
+ (task, index, self) =>
+ // Remove any duplicates just in case
+ index === self.findIndex((t) => t.id === task.id),
+ );
+ });
+ queryClient.invalidateQueries({ queryKey: projectKeys.taskCounts() });
+ showToast("Task created successfully", "success");
+ },
+ onSettled: (_data, _error, variables) => {
+ // Always refetch to ensure consistency after operation completes
+ queryClient.invalidateQueries({ queryKey: taskKeys.all(variables.project_id) });
+ },
+ });
+}
+
+// Update task mutation with optimistic updates
+export function useUpdateTask(projectId: string) {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+
+ return useMutation({
+ mutationFn: ({ taskId, updates }: { taskId: string; updates: UpdateTaskRequest }) =>
+ taskService.updateTask(taskId, updates),
+ onMutate: async ({ taskId, updates }) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: taskKeys.all(projectId) });
+
+ // Snapshot the previous value
+ const previousTasks = queryClient.getQueryData(taskKeys.all(projectId));
+
+ // Optimistically update
+ queryClient.setQueryData(taskKeys.all(projectId), (old: Task[] | undefined) => {
+ if (!old) return old;
+ return old.map((task: Task) => (task.id === taskId ? { ...task, ...updates } : task));
+ });
+
+ return { previousTasks };
+ },
+ onError: (error, variables, context) => {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ console.error("Failed to update task:", error, { variables });
+ // Rollback on error
+ if (context?.previousTasks) {
+ queryClient.setQueryData(taskKeys.all(projectId), context.previousTasks);
+ }
+ showToast(`Failed to update task: ${errorMessage}`, "error");
+ // Refetch on error to ensure consistency
+ queryClient.invalidateQueries({ queryKey: taskKeys.all(projectId) });
+ queryClient.invalidateQueries({ queryKey: projectKeys.taskCounts() });
+ },
+ onSuccess: (_, { updates }) => {
+ // Only invalidate counts if status changed (which affects counts)
+ if (updates.status) {
+ queryClient.invalidateQueries({ queryKey: projectKeys.taskCounts() });
+ // Show toast for significant status changes
+ showToast(`Task moved to ${updates.status}`, "success");
+ }
+ // Don't refetch task list - trust optimistic update
+ },
+ });
+}
+
+// Delete task mutation
+export function useDeleteTask(projectId: string) {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+
+ return useMutation({
+ mutationFn: (taskId: string) => taskService.deleteTask(taskId),
+ onMutate: async (taskId) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({ queryKey: taskKeys.all(projectId) });
+
+ // Snapshot the previous value
+ const previousTasks = queryClient.getQueryData(taskKeys.all(projectId));
+
+ // Optimistically remove the task
+ queryClient.setQueryData(taskKeys.all(projectId), (old) => {
+ if (!old) return old;
+ return old.filter((task) => task.id !== taskId);
+ });
+
+ return { previousTasks };
+ },
+ onError: (error, taskId, context) => {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ console.error("Failed to delete task:", error, { taskId });
+ // Rollback on error
+ if (context?.previousTasks) {
+ queryClient.setQueryData(taskKeys.all(projectId), context.previousTasks);
+ }
+ showToast(`Failed to delete task: ${errorMessage}`, "error");
+ },
+ onSuccess: () => {
+ showToast("Task deleted successfully", "success");
+ },
+ onSettled: () => {
+ // Always refetch counts after deletion
+ queryClient.invalidateQueries({ queryKey: projectKeys.taskCounts() });
+ },
+ });
+}
diff --git a/archon-ui-main/src/features/projects/tasks/index.ts b/archon-ui-main/src/features/projects/tasks/index.ts
new file mode 100644
index 0000000000..9b80dc0f8d
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Tasks Feature Module
+ *
+ * Sub-feature of projects for managing project tasks
+ */
+
+export * from "./components";
+export * from "./hooks";
+export { TasksTab } from "./TasksTab";
+export * from "./types";
diff --git a/archon-ui-main/src/features/projects/tasks/schemas/index.ts b/archon-ui-main/src/features/projects/tasks/schemas/index.ts
new file mode 100644
index 0000000000..aee7a4192f
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/schemas/index.ts
@@ -0,0 +1,80 @@
+import { z } from "zod";
+
+// Base validation schemas
+export const DatabaseTaskStatusSchema = z.enum(["todo", "doing", "review", "done"]);
+export const TaskPrioritySchema = z.enum(["low", "medium", "high", "critical"]);
+
+// Assignee schema - simplified to predefined options
+export const AssigneeSchema = z.enum(["User", "Archon", "AI IDE Agent"]);
+
+// Task schemas
+export const CreateTaskSchema = z.object({
+ project_id: z.string().uuid("Project ID must be a valid UUID"),
+ parent_task_id: z.string().uuid("Parent task ID must be a valid UUID").optional(),
+ title: z.string().min(1, "Task title is required").max(255, "Task title must be less than 255 characters"),
+ description: z.string().max(10000, "Task description must be less than 10000 characters").default(""),
+ status: DatabaseTaskStatusSchema.default("todo"),
+ assignee: AssigneeSchema.default("User"),
+ task_order: z.number().int().min(0).default(0),
+ feature: z.string().max(100, "Feature name must be less than 100 characters").optional(),
+ featureColor: z
+ .string()
+ .regex(/^#[0-9A-F]{6}$/i, "Feature color must be a valid hex color")
+ .optional(),
+ priority: TaskPrioritySchema.default("medium"),
+ sources: z.array(z.any()).default([]),
+ code_examples: z.array(z.any()).default([]),
+});
+
+export const UpdateTaskSchema = CreateTaskSchema.partial().omit({
+ project_id: true,
+});
+
+export const TaskSchema = z.object({
+ id: z.string().uuid("Task ID must be a valid UUID"),
+ project_id: z.string().uuid("Project ID must be a valid UUID"),
+ parent_task_id: z.string().uuid().optional(),
+ title: z.string().min(1),
+ description: z.string(),
+ status: DatabaseTaskStatusSchema,
+ assignee: AssigneeSchema,
+ task_order: z.number().int().min(0),
+ sources: z.array(z.any()).default([]),
+ code_examples: z.array(z.any()).default([]),
+ created_at: z.string().datetime(),
+ updated_at: z.string().datetime(),
+
+ // Extended UI properties
+ feature: z.string().optional(),
+ featureColor: z.string().optional(),
+ priority: TaskPrioritySchema.optional(),
+});
+
+// Update task status schema (for drag & drop operations)
+export const UpdateTaskStatusSchema = z.object({
+ task_id: z.string().uuid("Task ID must be a valid UUID"),
+ status: DatabaseTaskStatusSchema,
+});
+
+// Validation helper functions
+export function validateTask(data: unknown) {
+ return TaskSchema.safeParse(data);
+}
+
+export function validateCreateTask(data: unknown) {
+ return CreateTaskSchema.safeParse(data);
+}
+
+export function validateUpdateTask(data: unknown) {
+ return UpdateTaskSchema.safeParse(data);
+}
+
+export function validateUpdateTaskStatus(data: unknown) {
+ return UpdateTaskStatusSchema.safeParse(data);
+}
+
+// Export type inference helpers
+export type CreateTaskInput = z.infer;
+export type UpdateTaskInput = z.infer;
+export type UpdateTaskStatusInput = z.infer;
+export type TaskInput = z.infer;
diff --git a/archon-ui-main/src/features/projects/tasks/services/index.ts b/archon-ui-main/src/features/projects/tasks/services/index.ts
new file mode 100644
index 0000000000..85ba8c8e12
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/services/index.ts
@@ -0,0 +1,8 @@
+/**
+ * Task Services
+ *
+ * Service layer for task operations.
+ * Part of the vertical slice architecture migration.
+ */
+
+export { taskService } from "./taskService";
diff --git a/archon-ui-main/src/features/projects/tasks/services/taskService.ts b/archon-ui-main/src/features/projects/tasks/services/taskService.ts
new file mode 100644
index 0000000000..dd293618e1
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/services/taskService.ts
@@ -0,0 +1,194 @@
+/**
+ * Task Management Service
+ * Focused service for task CRUD operations only
+ */
+
+import { formatZodErrors, ValidationError } from "../../shared/api";
+import { callAPIWithETag, invalidateETagCache } from "../../shared/apiWithEtag";
+
+import { validateCreateTask, validateUpdateTask, validateUpdateTaskStatus } from "../schemas";
+import type { CreateTaskRequest, DatabaseTaskStatus, Task, TaskCounts, UpdateTaskRequest } from "../types";
+
+export const taskService = {
+ /**
+ * Get all tasks for a project
+ */
+ async getTasksByProject(projectId: string): Promise {
+ try {
+ const tasks = await callAPIWithETag(`/api/projects/${projectId}/tasks`);
+
+ // Convert database tasks to UI tasks with status mapping
+ return tasks;
+ } catch (error) {
+ console.error(`Failed to get tasks for project ${projectId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get a specific task by ID
+ */
+ async getTask(taskId: string): Promise {
+ try {
+ const task = await callAPIWithETag(`/api/tasks/${taskId}`);
+ return task;
+ } catch (error) {
+ console.error(`Failed to get task ${taskId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Create a new task
+ */
+ async createTask(taskData: CreateTaskRequest): Promise {
+ // Validate input
+ const validation = validateCreateTask(taskData);
+ if (!validation.success) {
+ throw new ValidationError(formatZodErrors(validation.error));
+ }
+
+ try {
+ // The validation.data already has defaults from schema
+ const requestData = validation.data;
+
+ const task = await callAPIWithETag("/api/tasks", {
+ method: "POST",
+ body: JSON.stringify(requestData),
+ });
+
+ // Invalidate task list cache for the project
+ invalidateETagCache(`/api/projects/${taskData.project_id}/tasks`);
+ invalidateETagCache("/api/tasks/counts");
+
+ return task;
+ } catch (error) {
+ console.error("Failed to create task:", error);
+ throw error;
+ }
+ },
+
+ /**
+ * Update an existing task
+ */
+ async updateTask(taskId: string, updates: UpdateTaskRequest): Promise {
+ // Validate input
+ const validation = validateUpdateTask(updates);
+ if (!validation.success) {
+ throw new ValidationError(formatZodErrors(validation.error));
+ }
+
+ try {
+ const task = await callAPIWithETag(`/api/tasks/${taskId}`, {
+ method: "PUT",
+ body: JSON.stringify(validation.data),
+ });
+
+ // Invalidate related caches
+ // Note: We don't know the project_id here, so TanStack Query will handle invalidation
+ invalidateETagCache("/api/tasks/counts");
+
+ return task;
+ } catch (error) {
+ console.error(`Failed to update task ${taskId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Update task status (for drag & drop operations)
+ */
+ async updateTaskStatus(taskId: string, status: DatabaseTaskStatus): Promise {
+ // Validate input
+ const validation = validateUpdateTaskStatus({
+ task_id: taskId,
+ status: status,
+ });
+ if (!validation.success) {
+ throw new ValidationError(formatZodErrors(validation.error));
+ }
+
+ try {
+ // Use the standard update task endpoint with JSON body
+ const task = await callAPIWithETag(`/api/tasks/${taskId}`, {
+ method: "PUT",
+ body: JSON.stringify({ status }),
+ });
+
+ // Invalidate task counts cache when status changes
+ invalidateETagCache("/api/tasks/counts");
+
+ return task;
+ } catch (error) {
+ console.error(`Failed to update task status ${taskId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Delete a task
+ */
+ async deleteTask(taskId: string): Promise {
+ try {
+ await callAPIWithETag(`/api/tasks/${taskId}`, {
+ method: "DELETE",
+ });
+
+ // Invalidate task counts cache after deletion
+ invalidateETagCache("/api/tasks/counts");
+ } catch (error) {
+ console.error(`Failed to delete task ${taskId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Update task order for better drag-and-drop support
+ */
+ async updateTaskOrder(taskId: string, newOrder: number, newStatus?: DatabaseTaskStatus): Promise {
+ try {
+ const updates: UpdateTaskRequest = {
+ task_order: newOrder,
+ };
+
+ if (newStatus) {
+ updates.status = newStatus;
+ }
+
+ const task = await this.updateTask(taskId, updates);
+
+ return task;
+ } catch (error) {
+ console.error(`Failed to update task order for ${taskId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get tasks by status across all projects
+ */
+ async getTasksByStatus(status: DatabaseTaskStatus): Promise {
+ try {
+ // Note: This method requires cross-project access
+ // For now, we'll throw an error suggesting to use project-scoped queries
+ throw new Error("getTasksByStatus requires cross-project access. Use getTasksByProject instead.");
+ } catch (error) {
+ console.error(`Failed to get tasks by status ${status}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get task counts for all projects in a single batch request
+ * Optimized endpoint to avoid N+1 query problem
+ */
+ async getTaskCountsForAllProjects(): Promise> {
+ try {
+ const response = await callAPIWithETag>("/api/projects/task-counts");
+ return response || {};
+ } catch (error) {
+ console.error("Failed to get task counts for all projects:", error);
+ throw error;
+ }
+ },
+};
diff --git a/archon-ui-main/src/features/projects/tasks/types/hooks.ts b/archon-ui-main/src/features/projects/tasks/types/hooks.ts
new file mode 100644
index 0000000000..4c0d162a42
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/types/hooks.ts
@@ -0,0 +1,46 @@
+/**
+ * Hook Type Definitions
+ *
+ * Type definitions for task-related hooks
+ */
+
+import type { Task } from "./task";
+
+/**
+ * Return type for useTaskActions hook
+ */
+export interface UseTaskActionsReturn {
+ // Actions
+ changeAssignee: (taskId: string, newAssignee: string) => void;
+ initiateDelete: (task: Task) => void;
+ confirmDelete: () => void;
+ cancelDelete: () => void;
+
+ // State
+ showDeleteConfirm: boolean;
+ taskToDelete: Task | null;
+
+ // Loading states
+ isUpdating: boolean;
+ isDeleting: boolean;
+}
+
+/**
+ * Return type for useTaskEditor hook
+ */
+export interface UseTaskEditorReturn {
+ // Data
+ projectFeatures: Array<{
+ id: string;
+ label: string;
+ type?: string;
+ color?: string;
+ }>;
+
+ // Actions
+ saveTask: (localTask: Partial | null, editingTask: Task | null, onSuccess?: () => void) => void;
+
+ // Loading states
+ isLoadingFeatures: boolean;
+ isSaving: boolean;
+}
diff --git a/archon-ui-main/src/features/projects/tasks/types/index.ts b/archon-ui-main/src/features/projects/tasks/types/index.ts
new file mode 100644
index 0000000000..343115738a
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/types/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Task Types
+ *
+ * All task-related types for the projects feature.
+ */
+
+// Hook return types
+export type { UseTaskActionsReturn, UseTaskEditorReturn } from "./hooks";
+// Core task types (vertical slice architecture)
+export type {
+ Assignee,
+ CreateTaskRequest,
+ DatabaseTaskStatus,
+ Task,
+ TaskCodeExample,
+ TaskCounts,
+ TaskPriority,
+ TaskSource,
+ UpdateTaskRequest,
+} from "./task";
diff --git a/archon-ui-main/src/features/projects/tasks/types/priority.ts b/archon-ui-main/src/features/projects/tasks/types/priority.ts
new file mode 100644
index 0000000000..de0951b1a9
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/types/priority.ts
@@ -0,0 +1,39 @@
+/**
+ * Priority System Types
+ *
+ * Defines user-facing priority levels separate from task_order (which handles drag-and-drop positioning).
+ * Priority is for display and user understanding, not for ordering logic.
+ */
+
+export type TaskPriority = "critical" | "high" | "medium" | "low";
+
+export interface TaskPriorityOption {
+ value: number; // Maps to task_order values for backwards compatibility
+ label: string;
+ color: string;
+}
+
+export const TASK_PRIORITY_OPTIONS: readonly TaskPriorityOption[] = [
+ { value: 1, label: "Critical", color: "text-red-600" },
+ { value: 25, label: "High", color: "text-orange-600" },
+ { value: 50, label: "Medium", color: "text-blue-600" },
+ { value: 100, label: "Low", color: "text-gray-600" },
+] as const;
+
+/**
+ * Convert task_order value to TaskPriority enum
+ */
+export function getTaskPriorityFromTaskOrder(taskOrder: number): TaskPriority {
+ if (taskOrder <= 1) return "critical";
+ if (taskOrder <= 25) return "high";
+ if (taskOrder <= 50) return "medium";
+ return "low";
+}
+
+/**
+ * Get task priority display properties from task_order
+ */
+export function getTaskPriorityOption(taskOrder: number): TaskPriorityOption {
+ const priority = TASK_PRIORITY_OPTIONS.find((p) => p.value >= taskOrder);
+ return priority || TASK_PRIORITY_OPTIONS[TASK_PRIORITY_OPTIONS.length - 1]; // Default to 'Low'
+}
diff --git a/archon-ui-main/src/features/projects/tasks/types/task.ts b/archon-ui-main/src/features/projects/tasks/types/task.ts
new file mode 100644
index 0000000000..3e88060c7b
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/types/task.ts
@@ -0,0 +1,93 @@
+/**
+ * Core Task Types
+ *
+ * Main task interfaces and types following vertical slice architecture
+ */
+
+// Import priority type from priority.ts to avoid duplication
+import type { TaskPriority } from "./priority";
+export type { TaskPriority };
+
+// Database status enum - using database values directly
+export type DatabaseTaskStatus = "todo" | "doing" | "review" | "done";
+
+// Assignee type - simplified to predefined options
+export type Assignee = "User" | "Archon" | "AI IDE Agent";
+
+// Task counts for project overview
+export interface TaskCounts {
+ todo: number;
+ doing: number;
+ review: number;
+ done: number;
+}
+
+// Task source and code example types (replacing any)
+export type TaskSource =
+ | {
+ url: string;
+ type: string;
+ relevance: string;
+ }
+ | Record;
+
+export type TaskCodeExample =
+ | {
+ file: string;
+ function: string;
+ purpose: string;
+ }
+ | Record;
+
+// Base Task interface (matches database schema)
+export interface Task {
+ id: string;
+ project_id: string;
+ title: string;
+ description: string;
+ status: DatabaseTaskStatus;
+ assignee: Assignee;
+ task_order: number;
+ feature?: string;
+ sources?: TaskSource[];
+ code_examples?: TaskCodeExample[];
+ created_at: string;
+ updated_at: string;
+
+ // Soft delete fields
+ archived?: boolean;
+ archived_at?: string;
+ archived_by?: string;
+
+ // Extended UI properties
+ featureColor?: string;
+ priority?: TaskPriority;
+}
+
+// Request types
+export interface CreateTaskRequest {
+ project_id: string;
+ title: string;
+ description: string;
+ status?: DatabaseTaskStatus;
+ assignee?: Assignee;
+ task_order?: number;
+ feature?: string;
+ featureColor?: string;
+ priority?: TaskPriority;
+ sources?: TaskSource[];
+ code_examples?: TaskCodeExample[];
+}
+
+export interface UpdateTaskRequest {
+ title?: string;
+ description?: string;
+ status?: DatabaseTaskStatus;
+ assignee?: Assignee;
+ task_order?: number;
+ feature?: string;
+ featureColor?: string;
+ priority?: TaskPriority;
+ sources?: TaskSource[];
+ code_examples?: TaskCodeExample[];
+}
diff --git a/archon-ui-main/src/features/projects/tasks/utils/index.ts b/archon-ui-main/src/features/projects/tasks/utils/index.ts
new file mode 100644
index 0000000000..670892e10f
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/utils/index.ts
@@ -0,0 +1,2 @@
+export * from "./task-ordering";
+export * from "./task-styles";
diff --git a/archon-ui-main/src/features/projects/tasks/utils/task-ordering.ts b/archon-ui-main/src/features/projects/tasks/utils/task-ordering.ts
new file mode 100644
index 0000000000..4b62d8498e
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/utils/task-ordering.ts
@@ -0,0 +1,100 @@
+/**
+ * Task ordering utilities that ensure integer precision
+ *
+ * Following alpha principles: detailed errors and no silent failures
+ */
+
+import type { Task } from "../types";
+
+export const ORDER_INCREMENT = 1000; // Large increment to avoid precision issues
+const MAX_ORDER = Number.MAX_SAFE_INTEGER - ORDER_INCREMENT;
+
+/**
+ * Calculate a default task order for new tasks in a status column
+ * Always returns an integer to avoid float precision issues
+ */
+export function getDefaultTaskOrder(existingTasks: Task[]): number {
+ if (existingTasks.length === 0) {
+ return ORDER_INCREMENT; // Start at 1000 for first task
+ }
+
+ // Find the maximum order in the existing tasks
+ const maxOrder = Math.max(...existingTasks.map((task) => task.task_order || 0));
+
+ // Ensure we don't exceed safe integer limits
+ if (maxOrder >= MAX_ORDER) {
+ throw new Error(`Task order limit exceeded. Maximum safe order is ${MAX_ORDER}, got ${maxOrder}`);
+ }
+
+ return maxOrder + ORDER_INCREMENT;
+}
+
+/**
+ * Calculate task order when inserting between two tasks
+ * Returns an integer that maintains proper ordering
+ */
+export function getInsertTaskOrder(beforeTask: Task | null, afterTask: Task | null): number {
+ const beforeOrder = beforeTask?.task_order || 0;
+ const afterOrder = afterTask?.task_order || beforeOrder + ORDER_INCREMENT * 2;
+
+ // If there's enough space between tasks, insert in the middle
+ const gap = afterOrder - beforeOrder;
+ if (gap > 1) {
+ const middleOrder = beforeOrder + Math.floor(gap / 2);
+ return middleOrder;
+ }
+
+ // If no gap, push everything after up by increment
+ return afterOrder + ORDER_INCREMENT;
+}
+
+/**
+ * Reorder a task within the same status column
+ * Ensures integer precision and proper spacing
+ */
+export function getReorderTaskOrder(tasks: Task[], taskId: string, newIndex: number): number {
+ const filteredTasks = tasks.filter((t) => t.id !== taskId);
+
+ if (filteredTasks.length === 0) {
+ return ORDER_INCREMENT;
+ }
+
+ // Sort tasks by current order
+ const sortedTasks = [...filteredTasks].sort((a, b) => (a.task_order || 0) - (b.task_order || 0));
+
+ // Handle edge cases
+ if (newIndex <= 0) {
+ // Moving to first position
+ const firstOrder = sortedTasks[0]?.task_order || ORDER_INCREMENT;
+ return Math.max(ORDER_INCREMENT, firstOrder - ORDER_INCREMENT);
+ }
+
+ if (newIndex >= sortedTasks.length) {
+ // Moving to last position
+ const lastOrder = sortedTasks[sortedTasks.length - 1]?.task_order || 0;
+ return lastOrder + ORDER_INCREMENT;
+ }
+
+ // Moving to middle position
+ const beforeTask = sortedTasks[newIndex - 1];
+ const afterTask = sortedTasks[newIndex];
+
+ return getInsertTaskOrder(beforeTask, afterTask);
+}
+
+/**
+ * Validate task order value
+ * Ensures it's a safe integer for database storage
+ */
+export function validateTaskOrder(order: number): number {
+ if (!Number.isInteger(order)) {
+ console.warn(`Task order ${order} is not an integer, rounding to ${Math.round(order)}`);
+ return Math.round(order);
+ }
+
+ if (order > MAX_ORDER || order < 0) {
+ throw new Error(`Task order ${order} is outside safe range [0, ${MAX_ORDER}]`);
+ }
+
+ return order;
+}
diff --git a/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx b/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx
new file mode 100644
index 0000000000..fd51981560
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx
@@ -0,0 +1,79 @@
+import { Bot, User } from "lucide-react";
+import type { Assignee } from "../types";
+
+// Drag and drop constants
+export const ItemTypes = {
+ TASK: "task",
+};
+
+// Get icon for assignee
+export const getAssigneeIcon = (assigneeName: Assignee) => {
+ switch (assigneeName) {
+ case "User":
+ return ;
+ case "AI IDE Agent":
+ return ;
+ case "Archon":
+ return ;
+ default:
+ return ;
+ }
+};
+
+// Get glow effect for assignee
+export const getAssigneeGlow = (assigneeName: Assignee) => {
+ switch (assigneeName) {
+ case "User":
+ return "shadow-[0_0_10px_rgba(59,130,246,0.4)]";
+ case "AI IDE Agent":
+ return "shadow-[0_0_10px_rgba(168,85,247,0.4)]";
+ case "Archon":
+ return "shadow-[0_0_10px_rgba(34,211,238,0.4)]";
+ default:
+ return "shadow-[0_0_10px_rgba(59,130,246,0.4)]";
+ }
+};
+
+// Get color based on task priority/order
+export const getOrderColor = (order: number) => {
+ if (order <= 3) return "bg-rose-500";
+ if (order <= 6) return "bg-orange-500";
+ if (order <= 10) return "bg-blue-500";
+ return "bg-emerald-500";
+};
+
+// Get glow effect based on task priority/order
+export const getOrderGlow = (order: number) => {
+ if (order <= 3) return "shadow-[0_0_10px_rgba(244,63,94,0.7)]";
+ if (order <= 6) return "shadow-[0_0_10px_rgba(249,115,22,0.7)]";
+ if (order <= 10) return "shadow-[0_0_10px_rgba(59,130,246,0.7)]";
+ return "shadow-[0_0_10px_rgba(16,185,129,0.7)]";
+};
+
+// Get column header color based on status
+export const getColumnColor = (status: "todo" | "doing" | "review" | "done") => {
+ switch (status) {
+ case "todo":
+ return "text-gray-600 dark:text-gray-400";
+ case "doing":
+ return "text-blue-600 dark:text-blue-400";
+ case "review":
+ return "text-purple-600 dark:text-purple-400";
+ case "done":
+ return "text-green-600 dark:text-green-400";
+ }
+};
+
+// Get column header glow based on status
+export const getColumnGlow = (status: "todo" | "doing" | "review" | "done") => {
+ switch (status) {
+ case "todo":
+ return "bg-gray-500/30";
+ case "doing":
+ 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 "done":
+ return "bg-green-500/30 shadow-[0_0_10px_2px_rgba(16,185,129,0.2)]";
+ }
+};
diff --git a/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx b/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx
new file mode 100644
index 0000000000..35c7334f74
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx
@@ -0,0 +1,59 @@
+import { useState } from "react";
+import { KanbanColumn } from "../components/KanbanColumn";
+import type { Task } from "../types";
+
+interface BoardViewProps {
+ tasks: Task[];
+ projectId: string;
+ onTaskMove: (taskId: string, newStatus: Task["status"]) => void;
+ onTaskReorder: (taskId: string, targetIndex: number, status: Task["status"]) => void;
+ onTaskEdit?: (task: Task) => void;
+ onTaskDelete?: (task: Task) => void;
+}
+
+export const BoardView = ({
+ tasks,
+ projectId,
+ onTaskMove,
+ onTaskReorder,
+ onTaskEdit,
+ onTaskDelete,
+}: BoardViewProps) => {
+ const [hoveredTaskId, setHoveredTaskId] = useState(null);
+
+ // 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);
+ };
+
+ // Column configuration
+ const columns: Array<{ status: Task["status"]; title: string }> = [
+ { status: "todo", title: "Todo" },
+ { status: "doing", title: "Doing" },
+ { status: "review", title: "Review" },
+ { status: "done", title: "Done" },
+ ];
+
+ return (
+
+ {/* Board Columns Grid */}
+
+ {columns.map(({ status, title }) => (
+
+ ))}
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/views/TableView.tsx b/archon-ui-main/src/features/projects/tasks/views/TableView.tsx
new file mode 100644
index 0000000000..49ac7940d8
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/views/TableView.tsx
@@ -0,0 +1,319 @@
+import { Check, Edit, Tag, Trash2 } from "lucide-react";
+import React, { useState } from "react";
+import { useDrag, useDrop } from "react-dnd";
+import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../ui/primitives";
+import { cn } from "../../../ui/primitives/styles";
+import { EditableTableCell } from "../components/EditableTableCell";
+import { TaskAssignee } from "../components/TaskAssignee";
+import { useDeleteTask, useUpdateTask } from "../hooks";
+import type { Assignee, Task } from "../types";
+import { getOrderColor, getOrderGlow, ItemTypes } from "../utils/task-styles";
+
+interface TableViewProps {
+ tasks: Task[];
+ projectId: string;
+ onTaskView?: (task: Task) => void;
+ onTaskComplete?: (taskId: string) => void;
+ onTaskDelete?: (task: Task) => void;
+ onTaskReorder: (taskId: string, newOrder: number, status: Task["status"]) => void;
+ onTaskUpdate?: (taskId: string, updates: Partial) => Promise;
+}
+
+interface DraggableRowProps {
+ task: Task;
+ index: number;
+ projectId: string;
+ onTaskView?: (task: Task) => void;
+ onTaskComplete?: (taskId: string) => void;
+ onTaskDelete?: (task: Task) => void;
+ onTaskReorder: (taskId: string, newOrder: number, status: Task["status"]) => void;
+}
+
+const DraggableRow = ({
+ task,
+ index,
+ projectId,
+ onTaskView,
+ onTaskComplete,
+ onTaskDelete,
+ onTaskReorder,
+}: DraggableRowProps) => {
+ const updateTaskMutation = useUpdateTask(projectId);
+ const deleteTaskMutation = useDeleteTask(projectId);
+ const [localAssignee, setLocalAssignee] = useState(task.assignee);
+
+ // Drag and drop handlers
+ const [{ isDragging }, drag] = useDrag({
+ type: ItemTypes.TASK,
+ item: { id: task.id, index, status: task.status },
+ collect: (monitor) => ({
+ isDragging: !!monitor.isDragging(),
+ }),
+ });
+
+ const [{ isOver }, drop] = useDrop({
+ accept: ItemTypes.TASK,
+ hover: (draggedItem: { id: string; index: number; status: Task["status"] }, 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;
+
+ // Move the task for visual feedback
+ onTaskReorder(draggedItem.id, hoveredIndex, task.status);
+
+ // Update the dragged item's index
+ draggedItem.index = hoveredIndex;
+ },
+ collect: (monitor) => ({
+ isOver: !!monitor.isOver(),
+ }),
+ });
+
+ // Handle field updates using mutations
+ const handleUpdateField = async (field: keyof Task, value: string) => {
+ const updates: Partial = { [field]: value };
+
+ await updateTaskMutation.mutateAsync({
+ taskId: task.id,
+ updates,
+ });
+ };
+
+ const handleAssigneeChange = (newAssignee: Assignee) => {
+ setLocalAssignee(newAssignee);
+ updateTaskMutation.mutate({
+ taskId: task.id,
+ updates: { assignee: newAssignee },
+ });
+ };
+
+ const handleDelete = () => {
+ if (onTaskDelete) {
+ onTaskDelete(task);
+ }
+ };
+
+ const handleComplete = () => {
+ if (onTaskComplete) {
+ onTaskComplete(task.id);
+ }
+ };
+
+ const handleEdit = () => {
+ if (onTaskView) {
+ onTaskView(task);
+ }
+ };
+
+ return (
+ drag(drop(node))}
+ className={cn(
+ "group transition-all duration-200 cursor-move",
+ index % 2 === 0 ? "bg-white/50 dark:bg-black/50" : "bg-gray-50/80 dark:bg-gray-900/30",
+ "hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70",
+ "dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20",
+ "border-b border-gray-200 dark:border-gray-800",
+ isDragging && "opacity-50 scale-105 shadow-lg",
+ isOver && "bg-cyan-100/50 dark:bg-cyan-900/20 border-cyan-400",
+ )}
+ >
+ {/* Priority/Order Indicator */}
+
+
+
+
+ {/* Title */}
+
+ handleUpdateField("title", value)}
+ placeholder="Enter task title"
+ className="font-medium"
+ isUpdating={updateTaskMutation.isPending}
+ />
+
+
+ {/* Status */}
+
+ handleUpdateField("status", value)}
+ type="status"
+ isUpdating={updateTaskMutation.isPending}
+ />
+
+
+ {/* Feature */}
+
+
+ {task.feature && }
+ handleUpdateField("feature", value)}
+ placeholder="Add feature"
+ className="text-sm"
+ isUpdating={updateTaskMutation.isPending}
+ />
+
+
+
+ {/* Assignee */}
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+ Edit task
+
+
+
+
+
+
+
+
+ Mark as complete
+
+
+
+
+
+
+
+
+ Delete task
+
+
+
+
+
+ );
+};
+
+export const TableView = ({
+ tasks,
+ projectId,
+ onTaskView,
+ onTaskComplete,
+ onTaskDelete,
+ onTaskReorder,
+}: TableViewProps) => {
+ // Group tasks by status for better organization
+ const groupedTasks = React.useMemo(() => {
+ const groups: Record = {
+ todo: [],
+ doing: [],
+ review: [],
+ done: [],
+ };
+
+ tasks.forEach((task) => {
+ groups[task.status].push(task);
+ });
+
+ // Sort each group by task_order
+ Object.keys(groups).forEach((status) => {
+ groups[status as Task["status"]].sort((a, b) => a.task_order - b.task_order);
+ });
+
+ return groups;
+ }, [tasks]);
+
+ const statusOrder: Task["status"][] = ["todo", "doing", "review", "done"];
+
+ return (
+
+
+
+
+
+ Title
+ Status
+ Feature
+ Assignee
+ Actions
+
+
+
+ {statusOrder.map((status) => {
+ const statusTasks = groupedTasks[status];
+ if (statusTasks.length === 0) return null;
+
+ return (
+
+ {/* Status group header */}
+
+
+
+
+ {status} ({statusTasks.length})
+
+
+
+
+ {/* Tasks in this status */}
+ {statusTasks.map((task, index) => (
+
+ ))}
+
+ );
+ })}
+ {tasks.length === 0 && (
+
+
+ No tasks yet. Create your first task to get started.
+
+
+ )}
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/projects/tasks/views/index.ts b/archon-ui-main/src/features/projects/tasks/views/index.ts
new file mode 100644
index 0000000000..8e665923ed
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/views/index.ts
@@ -0,0 +1,2 @@
+export { BoardView } from "./BoardView";
+export { TableView } from "./TableView";
diff --git a/archon-ui-main/src/features/projects/types/index.ts b/archon-ui-main/src/features/projects/types/index.ts
new file mode 100644
index 0000000000..71cb024560
--- /dev/null
+++ b/archon-ui-main/src/features/projects/types/index.ts
@@ -0,0 +1,25 @@
+/**
+ * Project Feature Types
+ *
+ * Central barrel export for all project-related types.
+ * Following vertical slice architecture - types are co-located with features.
+ */
+
+// Document-related types from documents feature
+export type * from "../documents/types";
+
+// Task-related types from tasks feature
+export type * from "../tasks/types";
+// Core project types (vertical slice architecture)
+export type {
+ CreateProjectRequest,
+ MCPToolResponse,
+ PaginatedResponse,
+ Project,
+ ProjectCreationProgress,
+ ProjectData,
+ ProjectDocs,
+ ProjectFeatures,
+ ProjectPRD,
+ UpdateProjectRequest,
+} from "./project";
diff --git a/archon-ui-main/src/features/projects/types/project.ts b/archon-ui-main/src/features/projects/types/project.ts
new file mode 100644
index 0000000000..687c14193e
--- /dev/null
+++ b/archon-ui-main/src/features/projects/types/project.ts
@@ -0,0 +1,107 @@
+/**
+ * Core Project Types
+ *
+ * Properly typed project interfaces following vertical slice architecture
+ */
+
+// Project JSONB field types - replacing any with proper unions
+export type ProjectPRD = Record;
+export type ProjectDocs = unknown[]; // Will be refined to ProjectDocument[] when fully migrated
+export type ProjectFeature = {
+ id: string;
+ label: string;
+ type?: string;
+ color?: string;
+};
+
+export type ProjectFeatures = ProjectFeature[];
+export type ProjectData = unknown[];
+
+// Project creation progress tracking
+export interface ProjectCreationProgress {
+ progressId: string;
+ status:
+ | "starting"
+ | "initializing_agents"
+ | "generating_docs"
+ | "processing_requirements"
+ | "ai_generation"
+ | "finalizing_docs"
+ | "saving_to_database"
+ | "completed"
+ | "error";
+ percentage: number;
+ logs: string[];
+ error?: string;
+ step?: string;
+ currentStep?: string;
+ eta?: string;
+ duration?: string;
+ project?: Project; // Forward reference - will be resolved
+}
+
+// Base Project interface (matches database schema)
+export interface Project {
+ id: string;
+ title: string;
+ prd?: ProjectPRD;
+ docs?: ProjectDocs;
+ features?: ProjectFeatures;
+ data?: ProjectData;
+ github_repo?: string;
+ created_at: string;
+ updated_at: string;
+ technical_sources?: string[];
+ business_sources?: string[];
+
+ // Extended UI properties
+ description?: string;
+ progress?: number;
+ updated?: string; // Human-readable format
+ pinned: boolean;
+
+ // Creation progress tracking for inline display
+ creationProgress?: ProjectCreationProgress;
+}
+
+// Request types
+export interface CreateProjectRequest {
+ title: string;
+ description?: string;
+ github_repo?: string;
+ pinned?: boolean;
+ docs?: ProjectDocs;
+ features?: ProjectFeatures;
+ data?: ProjectData;
+ technical_sources?: string[];
+ business_sources?: string[];
+}
+
+export interface UpdateProjectRequest {
+ title?: string;
+ description?: string;
+ github_repo?: string;
+ prd?: ProjectPRD;
+ docs?: ProjectDocs;
+ features?: ProjectFeatures;
+ data?: ProjectData;
+ technical_sources?: string[];
+ business_sources?: string[];
+ pinned?: boolean;
+}
+
+// Utility types
+export interface MCPToolResponse {
+ success: boolean;
+ data?: T;
+ error?: string;
+ message?: string;
+}
+
+export interface PaginatedResponse {
+ items: T[];
+ total: number;
+ page: number;
+ limit: number;
+ hasMore: boolean;
+}
diff --git a/archon-ui-main/src/features/projects/utils/index.ts b/archon-ui-main/src/features/projects/utils/index.ts
new file mode 100644
index 0000000000..8a575332d7
--- /dev/null
+++ b/archon-ui-main/src/features/projects/utils/index.ts
@@ -0,0 +1,12 @@
+/**
+ * Project Utilities
+ *
+ * Shared utility functions for the projects feature.
+ * Includes:
+ * - Task status helpers
+ * - Date formatting
+ * - Project validation
+ * - Constants and enums
+ */
+
+// Utilities will be exported here as they're migrated
diff --git a/archon-ui-main/src/features/projects/views/ProjectsView.tsx b/archon-ui-main/src/features/projects/views/ProjectsView.tsx
new file mode 100644
index 0000000000..60f18eebff
--- /dev/null
+++ b/archon-ui-main/src/features/projects/views/ProjectsView.tsx
@@ -0,0 +1,239 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { motion } from "framer-motion";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useStaggeredEntrance } from "../../../hooks/useStaggeredEntrance";
+import { DeleteConfirmModal } from "../../ui/components/DeleteConfirmModal";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives";
+import { NewProjectModal } from "../components/NewProjectModal";
+import { ProjectHeader } from "../components/ProjectHeader";
+import { ProjectList } from "../components/ProjectList";
+import { DocsTab } from "../documents/DocsTab";
+import {
+ projectKeys,
+ useDeleteProject,
+ useProjects,
+ useTaskCounts,
+ useUpdateProject,
+} from "../hooks/useProjectQueries";
+import { TasksTab } from "../tasks/TasksTab";
+import type { Project } from "../types";
+
+interface ProjectsViewProps {
+ className?: string;
+ "data-id"?: string;
+}
+
+const containerVariants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: { staggerChildren: 0.1 },
+ },
+};
+
+const itemVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: [0.23, 1, 0.32, 1] },
+ },
+};
+
+export function ProjectsView({ className = "", "data-id": dataId }: ProjectsViewProps) {
+ const { projectId } = useParams();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+
+ // State management
+ const [selectedProject, setSelectedProject] = useState(null);
+ const [activeTab, setActiveTab] = useState("tasks");
+ const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [projectToDelete, setProjectToDelete] = useState<{
+ id: string;
+ title: string;
+ } | null>(null);
+
+ // React Query hooks
+ const { data: projects = [], isLoading: isLoadingProjects, error: projectsError } = useProjects();
+ const { data: taskCounts = {}, refetch: refetchTaskCounts } = useTaskCounts();
+
+ // Mutations
+ const updateProjectMutation = useUpdateProject();
+ const deleteProjectMutation = useDeleteProject();
+
+ // Sort projects - pinned first, then alphabetically
+ const sortedProjects = useMemo(() => {
+ return [...(projects as Project[])].sort((a, b) => {
+ if (a.pinned && !b.pinned) return -1;
+ if (!a.pinned && b.pinned) return 1;
+ return a.title.localeCompare(b.title);
+ });
+ }, [projects]);
+
+ // Handle project selection
+ const handleProjectSelect = useCallback(
+ (project: Project) => {
+ if (selectedProject?.id === project.id) return;
+
+ setSelectedProject(project);
+ setActiveTab("tasks");
+ navigate(`/projects/${project.id}`, { replace: true });
+ },
+ [selectedProject?.id, navigate],
+ );
+
+ // Auto-select project based on URL or default to leftmost
+ useEffect(() => {
+ if (!sortedProjects.length) return;
+
+ // If there's a projectId in the URL, select that project
+ if (projectId) {
+ const project = sortedProjects.find((p) => p.id === projectId);
+ if (project) {
+ setSelectedProject(project);
+ return;
+ }
+ }
+
+ // Otherwise, select the first (leftmost) project
+ if (!selectedProject || !sortedProjects.find((p) => p.id === selectedProject.id)) {
+ const defaultProject = sortedProjects[0];
+ setSelectedProject(defaultProject);
+ navigate(`/projects/${defaultProject.id}`, { replace: true });
+ }
+ }, [sortedProjects, projectId, selectedProject, navigate]);
+
+ // Refetch task counts when projects change
+ useEffect(() => {
+ if ((projects as Project[]).length > 0) {
+ refetchTaskCounts();
+ }
+ }, [projects, refetchTaskCounts]);
+
+ // Handle pin toggle
+ const handlePinProject = async (e: React.MouseEvent, projectId: string) => {
+ e.stopPropagation();
+ const project = (projects as Project[]).find((p) => p.id === projectId);
+ if (!project) return;
+
+ updateProjectMutation.mutate({
+ projectId,
+ updates: { pinned: !project.pinned },
+ });
+ };
+
+ // Handle delete project
+ const handleDeleteProject = (e: React.MouseEvent, projectId: string, title: string) => {
+ e.stopPropagation();
+ setProjectToDelete({ id: projectId, title });
+ setShowDeleteConfirm(true);
+ };
+
+ const confirmDeleteProject = () => {
+ if (!projectToDelete) return;
+
+ deleteProjectMutation.mutate(projectToDelete.id, {
+ onSuccess: () => {
+ // Success toast handled by mutation
+ setShowDeleteConfirm(false);
+ setProjectToDelete(null);
+
+ // If we deleted the selected project, select another one
+ if (selectedProject?.id === projectToDelete.id) {
+ const remainingProjects = (projects as Project[]).filter((p) => p.id !== projectToDelete.id);
+ if (remainingProjects.length > 0) {
+ const nextProject = remainingProjects[0];
+ setSelectedProject(nextProject);
+ navigate(`/projects/${nextProject.id}`, { replace: true });
+ } else {
+ setSelectedProject(null);
+ navigate("/projects", { replace: true });
+ }
+ }
+ },
+ });
+ };
+
+ const cancelDeleteProject = () => {
+ setShowDeleteConfirm(false);
+ setProjectToDelete(null);
+ };
+
+ // Staggered entrance animation
+ const isVisible = useStaggeredEntrance([1, 2, 3], 0.15);
+
+ return (
+
+ setIsNewProjectModalOpen(true)} />
+
+ queryClient.invalidateQueries({ queryKey: projectKeys.lists() })}
+ />
+
+ {/* Project Details Section */}
+ {selectedProject && (
+
+
+
+
+ Docs
+
+
+ Tasks
+
+
+
+ {/* Tab content */}
+
+ {activeTab === "docs" && (
+
+
+
+ )}
+ {activeTab === "tasks" && (
+
+
+
+ )}
+
+
+
+ )}
+
+ {/* Modals */}
+ refetchTaskCounts()}
+ />
+
+ {showDeleteConfirm && projectToDelete && (
+
+ )}
+
+ );
+}
diff --git a/archon-ui-main/src/features/projects/views/ProjectsViewWithBoundary.tsx b/archon-ui-main/src/features/projects/views/ProjectsViewWithBoundary.tsx
new file mode 100644
index 0000000000..c760a6d6e6
--- /dev/null
+++ b/archon-ui-main/src/features/projects/views/ProjectsViewWithBoundary.tsx
@@ -0,0 +1,15 @@
+import { QueryErrorResetBoundary } from "@tanstack/react-query";
+import { FeatureErrorBoundary } from "../../ui/components";
+import { ProjectsView } from "./ProjectsView";
+
+export const ProjectsViewWithBoundary = () => {
+ return (
+
+ {({ reset }) => (
+
+
+
+ )}
+
+ );
+};
diff --git a/archon-ui-main/src/features/testing/test-utils.tsx b/archon-ui-main/src/features/testing/test-utils.tsx
new file mode 100644
index 0000000000..fbdb1e5b9e
--- /dev/null
+++ b/archon-ui-main/src/features/testing/test-utils.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { render as rtlRender } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ToastProvider } from '../ui/components/ToastProvider';
+import { TooltipProvider } from '../ui/primitives/tooltip';
+
+/**
+ * Custom render function that wraps components with all necessary providers
+ * This follows the best practice of having a centralized test render utility
+ */
+export function renderWithProviders(
+ ui: React.ReactElement,
+ {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ }),
+ ...renderOptions
+ } = {}
+) {
+ function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+
+ return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
+}
+
+// Re-export everything from React Testing Library
+export * from '@testing-library/react';
+
+// Override the default render with our custom one
+export { renderWithProviders as render };
\ No newline at end of file
diff --git a/archon-ui-main/src/features/ui/components/DeleteConfirmModal.tsx b/archon-ui-main/src/features/ui/components/DeleteConfirmModal.tsx
new file mode 100644
index 0000000000..4787d63ff4
--- /dev/null
+++ b/archon-ui-main/src/features/ui/components/DeleteConfirmModal.tsx
@@ -0,0 +1,138 @@
+import { Trash2 } from "lucide-react";
+import type React from "react";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "../primitives/alert-dialog";
+import { Button } from "../primitives/button";
+import { cn } from "../primitives/styles";
+
+interface DeleteConfirmModalProps {
+ itemName: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ type: "project" | "task" | "client" | "document";
+ size?: "compact" | "default" | "large";
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
+
+export const DeleteConfirmModal: React.FC = ({
+ itemName,
+ onConfirm,
+ onCancel,
+ type,
+ size = "default",
+ open = false,
+ onOpenChange,
+}) => {
+ const TITLES: Record = {
+ project: "Delete Project",
+ task: "Delete Task",
+ client: "Delete MCP Client",
+ document: "Delete Document",
+ };
+
+ const MESSAGES: Record string> = {
+ project: (_n) => `Are you sure you want to delete this project?`,
+ task: (_n) => `Are you sure you want to delete this task?`,
+ client: (_n) => `Are you sure you want to delete this client?`,
+ document: (_n) => `Are you sure you want to delete this document?`,
+ };
+
+ // Size-specific styling for icon
+ const getIconStyles = () => {
+ switch (size) {
+ case "compact":
+ return { container: "w-8 h-8", icon: "w-4 h-4" };
+ case "large":
+ return { container: "w-16 h-16", icon: "w-8 h-8" };
+ default:
+ return { container: "w-12 h-12", icon: "w-6 h-6" };
+ }
+ };
+
+ const iconStyles = getIconStyles();
+
+ return (
+ !o && onCancel())}>
+
+
+
+
+
+
+
+
+ {TITLES[type]}
+
+
+ This action cannot be undone
+
+
+
+
+ {MESSAGES[type](itemName)}
+
+
+
+
+
+ Cancel
+
+
+
+
+ Delete
+
+
+
+
+
+ );
+};
diff --git a/archon-ui-main/src/features/ui/components/FeatureErrorBoundary.tsx b/archon-ui-main/src/features/ui/components/FeatureErrorBoundary.tsx
new file mode 100644
index 0000000000..c9a0c6d86b
--- /dev/null
+++ b/archon-ui-main/src/features/ui/components/FeatureErrorBoundary.tsx
@@ -0,0 +1,123 @@
+import { AlertTriangle, RefreshCw } from "lucide-react";
+import { Component, type ErrorInfo, type ReactNode } from "react";
+import { Button } from "../primitives";
+import { cn, glassmorphism } from "../primitives/styles";
+
+interface Props {
+ children: ReactNode;
+ featureName: string;
+ onReset?: () => void;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+ errorInfo: ErrorInfo | null;
+}
+
+export class FeatureErrorBoundary extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { hasError: false, error: null, errorInfo: null };
+ }
+
+ static getDerivedStateFromError(error: Error): Partial {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ // Log detailed error information for debugging in dev/test
+ if (import.meta.env.DEV || import.meta.env.MODE === "test") {
+ // biome-ignore lint: intentional diagnostic log in development
+ console.error(`Feature Error in ${this.props.featureName}:`, {
+ error,
+ errorInfo,
+ componentStack: errorInfo.componentStack,
+ errorMessage: error.message,
+ errorStack: error.stack,
+ timestamp: new Date().toISOString(),
+ });
+ }
+
+ this.setState({
+ error,
+ errorInfo,
+ });
+ }
+
+ handleReset = () => {
+ this.setState({ hasError: false, error: null, errorInfo: null });
+ this.props.onReset?.();
+ };
+
+ render() {
+ if (this.state.hasError) {
+ const { error, errorInfo } = this.state;
+ const isDevelopment = process.env.NODE_ENV === "development";
+
+ return (
+
+
+
+
+
+
+
+ Feature Error: {this.props.featureName}
+
+
+
+ An error occurred in this feature. The error has been logged for investigation.
+
+
+ {/* Show detailed error in alpha/development (following CLAUDE.md principles) */}
+ {isDevelopment && error && (
+
+
{error.toString()}
+ {error.stack && (
+
{error.stack}
+ )}
+ {errorInfo?.componentStack && (
+
+
Component Stack:
+
+ {errorInfo.componentStack}
+
+
+ )}
+
+ )}
+
+
+
+ Try Again
+
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/archon-ui-main/src/features/ui/components/ToastProvider.tsx b/archon-ui-main/src/features/ui/components/ToastProvider.tsx
new file mode 100644
index 0000000000..6cf529b572
--- /dev/null
+++ b/archon-ui-main/src/features/ui/components/ToastProvider.tsx
@@ -0,0 +1,54 @@
+import type React from "react";
+import { createToastContext, getToastIcon, ToastContext } from "../hooks/useToast";
+import {
+ ToastProvider as RadixToastProvider,
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastViewport,
+} from "../primitives/toast";
+
+interface ToastProviderProps {
+ children: React.ReactNode;
+ duration?: number;
+ swipeDirection?: "right" | "left" | "up" | "down";
+}
+
+/**
+ * Toast Provider Component
+ * Wraps the app with Radix ToastProvider and manages toast state
+ * Provides the same API as legacy ToastContext for easy migration
+ */
+export function ToastProvider({ children, duration = 4000, swipeDirection = "right" }: ToastProviderProps) {
+ const { toasts, showToast, removeToast } = createToastContext();
+
+ return (
+
+
+ {children}
+ {toasts.map((toast) => {
+ const Icon = getToastIcon(toast.type);
+ const variantMap = {
+ success: "success" as const,
+ error: "error" as const,
+ warning: "warning" as const,
+ info: "default" as const,
+ };
+
+ return (
+
+
+ {Icon &&
}
+
+ {toast.message}
+
+
+ removeToast(toast.id)} />
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/archon-ui-main/src/features/ui/components/index.ts b/archon-ui-main/src/features/ui/components/index.ts
new file mode 100644
index 0000000000..e4287ac456
--- /dev/null
+++ b/archon-ui-main/src/features/ui/components/index.ts
@@ -0,0 +1,2 @@
+export { DeleteConfirmModal } from "./DeleteConfirmModal";
+export { FeatureErrorBoundary } from "./FeatureErrorBoundary";
diff --git a/archon-ui-main/src/features/ui/hooks/index.ts b/archon-ui-main/src/features/ui/hooks/index.ts
new file mode 100644
index 0000000000..98ba18bc10
--- /dev/null
+++ b/archon-ui-main/src/features/ui/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from "./useSmartPolling";
+export * from "./useThemeAware";
diff --git a/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts b/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts
new file mode 100644
index 0000000000..7c84c40ea1
--- /dev/null
+++ b/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts
@@ -0,0 +1,189 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useSmartPolling } from '../useSmartPolling';
+
+describe('useSmartPolling', () => {
+ beforeEach(() => {
+ // Reset document visibility state
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ configurable: true,
+ });
+ Object.defineProperty(document, 'hidden', {
+ value: false,
+ writable: true,
+ configurable: true,
+ });
+ // Mock document.hasFocus
+ document.hasFocus = vi.fn(() => true);
+ });
+
+ afterEach(() => {
+ vi.clearAllTimers();
+ vi.clearAllMocks();
+ });
+
+ it('should return the base interval when document is visible and focused', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ expect(result.current.refetchInterval).toBe(5000);
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.isVisible).toBe(true);
+ expect(result.current.hasFocus).toBe(true);
+ });
+
+ it('should disable polling when document is hidden', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Initially should be active
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.refetchInterval).toBe(5000);
+
+ // Simulate tab becoming hidden
+ act(() => {
+ Object.defineProperty(document, 'hidden', {
+ value: true,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ // Should be disabled (returns false)
+ expect(result.current.isVisible).toBe(false);
+ expect(result.current.isActive).toBe(false);
+ expect(result.current.refetchInterval).toBe(false);
+ });
+
+ it('should resume polling when document becomes visible again', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Make hidden
+ act(() => {
+ Object.defineProperty(document, 'hidden', {
+ value: true,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ expect(result.current.refetchInterval).toBe(false);
+
+ // Make visible again
+ act(() => {
+ Object.defineProperty(document, 'hidden', {
+ value: false,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ expect(result.current.isVisible).toBe(true);
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.refetchInterval).toBe(5000);
+ });
+
+ it('should slow down to 60 seconds when window loses focus', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Initially focused
+ expect(result.current.refetchInterval).toBe(5000);
+ expect(result.current.hasFocus).toBe(true);
+
+ // Simulate window blur
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+
+ // Should be slowed down to 60 seconds
+ expect(result.current.hasFocus).toBe(false);
+ expect(result.current.isActive).toBe(false);
+ expect(result.current.refetchInterval).toBe(60000);
+ });
+
+ it('should resume normal speed when window regains focus', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Blur window
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+
+ expect(result.current.refetchInterval).toBe(60000);
+
+ // Focus window again
+ act(() => {
+ window.dispatchEvent(new Event('focus'));
+ });
+
+ expect(result.current.hasFocus).toBe(true);
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.refetchInterval).toBe(5000);
+ });
+
+ it('should handle different base intervals', () => {
+ const { result: result1 } = renderHook(() => useSmartPolling(1000));
+ const { result: result2 } = renderHook(() => useSmartPolling(10000));
+
+ expect(result1.current.refetchInterval).toBe(1000);
+ expect(result2.current.refetchInterval).toBe(10000);
+
+ // When blurred, both should be 60 seconds
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+
+ expect(result1.current.refetchInterval).toBe(60000);
+ expect(result2.current.refetchInterval).toBe(60000);
+ });
+
+ it('should use default interval of 10000ms when not specified', () => {
+ const { result } = renderHook(() => useSmartPolling());
+
+ expect(result.current.refetchInterval).toBe(10000);
+ });
+
+ it('should cleanup event listeners on unmount', () => {
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
+ const windowRemoveEventListenerSpy = vi.spyOn(window, 'removeEventListener');
+
+ const { unmount } = renderHook(() => useSmartPolling(5000));
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function));
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('blur', expect.any(Function));
+
+ removeEventListenerSpy.mockRestore();
+ windowRemoveEventListenerSpy.mockRestore();
+ });
+
+ it('should correctly report isActive state', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Active when both visible and focused
+ expect(result.current.isActive).toBe(true);
+
+ // Not active when not focused
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+ expect(result.current.isActive).toBe(false);
+
+ // Not active when hidden
+ act(() => {
+ window.dispatchEvent(new Event('focus')); // Focus first
+ Object.defineProperty(document, 'hidden', {
+ value: true,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+ expect(result.current.isActive).toBe(false);
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/ui/hooks/useSmartPolling.ts b/archon-ui-main/src/features/ui/hooks/useSmartPolling.ts
new file mode 100644
index 0000000000..0a286e9384
--- /dev/null
+++ b/archon-ui-main/src/features/ui/hooks/useSmartPolling.ts
@@ -0,0 +1,66 @@
+import { useEffect, useState } from "react";
+
+/**
+ * Smart polling hook that adjusts interval based on page visibility and focus
+ *
+ * Reduces unnecessary API calls when user is not actively using the app
+ */
+export function useSmartPolling(baseInterval: number = 10000) {
+ const [isVisible, setIsVisible] = useState(true);
+ const [hasFocus, setHasFocus] = useState(true);
+
+ useEffect(() => {
+ // Guard against SSR and non-browser environments
+ if (typeof document === "undefined" || typeof window === "undefined") {
+ return;
+ }
+
+ const handleVisibilityChange = () => {
+ setIsVisible(!document.hidden);
+ };
+
+ const handleFocus = () => setHasFocus(true);
+ const handleBlur = () => setHasFocus(false);
+
+ // Set initial state
+ setIsVisible(!document.hidden);
+ setHasFocus(document.hasFocus());
+
+ // Add event listeners
+ document.addEventListener("visibilitychange", handleVisibilityChange);
+ window.addEventListener("focus", handleFocus);
+ window.addEventListener("blur", handleBlur);
+
+ return () => {
+ // Cleanup with same guards
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
+ window.removeEventListener("focus", handleFocus);
+ window.removeEventListener("blur", handleBlur);
+ }
+ };
+ }, []);
+
+ // Calculate smart interval based on visibility and focus
+ const getSmartInterval = (): number | false => {
+ if (!isVisible) {
+ // Page is hidden - disable polling
+ return false;
+ }
+
+ if (!hasFocus) {
+ // Page is visible but not focused - poll less frequently (1 minute)
+ return 60000; // 60 seconds for background polling
+ }
+
+ // Page is active - use normal interval
+ return baseInterval;
+ };
+
+ return {
+ refetchInterval: getSmartInterval(),
+ isActive: isVisible && hasFocus,
+ isVisible,
+ hasFocus,
+ };
+}
diff --git a/archon-ui-main/src/features/ui/hooks/useThemeAware.ts b/archon-ui-main/src/features/ui/hooks/useThemeAware.ts
new file mode 100644
index 0000000000..f03fcb9082
--- /dev/null
+++ b/archon-ui-main/src/features/ui/hooks/useThemeAware.ts
@@ -0,0 +1,70 @@
+/**
+ * Theme-aware utilities for Radix primitives
+ * Works with existing ThemeContext
+ */
+
+import { useTheme } from "../../../contexts/ThemeContext";
+
+export function useThemeAware() {
+ const { theme, setTheme } = useTheme();
+ const isDark = theme === "dark";
+ const isLight = theme === "light";
+
+ // Get theme-specific values
+ const getThemeValue = (lightValue: T, darkValue: T): T => {
+ return isDark ? darkValue : lightValue;
+ };
+
+ // Get theme-specific colors for Tron effects
+ const glowColors = {
+ cyan: isDark
+ ? "rgba(34,211,238,0.7)" // Stronger glow in dark
+ : "rgba(34,211,238,0.4)", // Softer glow in light
+ blue: isDark ? "rgba(59,130,246,0.7)" : "rgba(59,130,246,0.4)",
+ purple: isDark ? "rgba(168,85,247,0.7)" : "rgba(168,85,247,0.4)",
+ };
+
+ // Get appropriate backdrop blur intensity
+ const blurIntensity = isDark ? "backdrop-blur-md" : "backdrop-blur-sm";
+
+ return {
+ theme,
+ setTheme,
+ isDark,
+ isLight,
+ getThemeValue,
+ glowColors,
+ blurIntensity,
+ };
+}
+
+// Theme-aware style presets for consistent look
+export const themeStyles = {
+ // Card styles that adapt to theme
+ card: {
+ light: "bg-gradient-to-b from-white/80 to-white/60 border-gray-200",
+ dark: "bg-gradient-to-b from-white/10 to-black/30 border-gray-700",
+ both: "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",
+ },
+
+ // Panel styles (dropdowns, modals, etc.)
+ panel: {
+ light: "bg-gradient-to-b from-white/95 to-white/90 border-gray-200",
+ dark: "bg-gradient-to-b from-gray-800/95 to-gray-900/95 border-gray-700",
+ both: "bg-gradient-to-b from-white/95 to-white/90 dark:from-gray-800/95 dark:to-gray-900/95 border border-gray-200 dark:border-gray-700",
+ },
+
+ // Text colors
+ text: {
+ primary: "text-gray-900 dark:text-white",
+ secondary: "text-gray-600 dark:text-gray-400",
+ muted: "text-gray-500 dark:text-gray-500",
+ },
+
+ // Glow effects for Tron aesthetic
+ glow: {
+ cyan: "shadow-[0_0_10px_2px_rgba(34,211,238,0.4)] dark:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]",
+ blue: "shadow-[0_0_10px_2px_rgba(59,130,246,0.4)] dark:shadow-[0_0_20px_5px_rgba(59,130,246,0.7)]",
+ purple: "shadow-[0_0_10px_2px_rgba(168,85,247,0.4)] dark:shadow-[0_0_20px_5px_rgba(168,85,247,0.7)]",
+ },
+};
diff --git a/archon-ui-main/src/features/ui/hooks/useToast.ts b/archon-ui-main/src/features/ui/hooks/useToast.ts
new file mode 100644
index 0000000000..5dd88c00d1
--- /dev/null
+++ b/archon-ui-main/src/features/ui/hooks/useToast.ts
@@ -0,0 +1,82 @@
+import { AlertCircle, CheckCircle, Info, XCircle } from "lucide-react";
+import { createContext, useCallback, useContext, useState } from "react";
+
+// Toast types
+interface Toast {
+ id: string;
+ message: string;
+ type: "success" | "error" | "info" | "warning";
+ duration?: number;
+}
+
+// Toast context type
+interface ToastContextType {
+ showToast: (message: string, type?: Toast["type"], duration?: number) => void;
+}
+
+// Create context
+const ToastContext = createContext(undefined);
+
+/**
+ * Hook to show toast notifications
+ * Provides the same API as legacy ToastContext for easy migration
+ */
+export function useToast() {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error("useToast must be used within a ToastProvider");
+ }
+ return context;
+}
+
+/**
+ * Create toast context value with state management
+ * Used internally by ToastProvider component
+ */
+export function createToastContext() {
+ const [toasts, setToasts] = useState([]);
+
+ const showToast = useCallback((message: string, type: Toast["type"] = "info", duration = 4000) => {
+ const id = Date.now().toString();
+ const newToast: Toast = { id, message, type, duration };
+
+ setToasts((prev) => [...prev, newToast]);
+
+ // Auto-dismiss after duration
+ if (duration > 0) {
+ setTimeout(() => {
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
+ }, duration);
+ }
+ }, []);
+
+ const removeToast = useCallback((id: string) => {
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
+ }, []);
+
+ return {
+ toasts,
+ showToast,
+ removeToast,
+ };
+}
+
+// Export context for provider
+export { ToastContext };
+
+// Export toast type for external use
+export type { Toast, ToastContextType };
+
+// Helper to get icon for toast type
+export function getToastIcon(type: Toast["type"]) {
+ switch (type) {
+ case "success":
+ return CheckCircle;
+ case "error":
+ return XCircle;
+ case "info":
+ return Info;
+ case "warning":
+ return AlertCircle;
+ }
+}
diff --git a/archon-ui-main/src/features/ui/primitives/alert-dialog.tsx b/archon-ui-main/src/features/ui/primitives/alert-dialog.tsx
new file mode 100644
index 0000000000..be18d41329
--- /dev/null
+++ b/archon-ui-main/src/features/ui/primitives/alert-dialog.tsx
@@ -0,0 +1,136 @@
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import React from "react";
+import { cn, glassmorphism } from "./styles";
+
+// Root
+export const AlertDialog = AlertDialogPrimitive.Root;
+
+// Trigger
+export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+// Portal
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+// Overlay with backdrop blur
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+// Content with Tron glassmorphism
+export const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ variant?: "default" | "destructive";
+ }
+>(({ className, variant = "default", ...props }, ref) => {
+ const variantStyles = {
+ default: cn(
+ "before:bg-gradient-to-r before:from-cyan-500 before:to-fuchsia-500",
+ "before:shadow-[0_0_10px_2px_rgba(34,211,238,0.4)]",
+ "dark:before:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]",
+ ),
+ destructive: cn(
+ "before:bg-red-500",
+ "before:shadow-[0_0_10px_2px_rgba(239,68,68,0.4)]",
+ "dark:before:shadow-[0_0_20px_5px_rgba(239,68,68,0.7)]",
+ ),
+ };
+
+ return (
+
+
+
+
+ );
+});
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+// Header
+export const AlertDialogHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
+
+// Footer
+export const AlertDialogFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
+
+// Title
+export const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+