diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8db17da..1313c6e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,15 +1,14 @@ { - "enabledPlugins": { - "dss-claude-plugin@local": true - }, - "localPlugins": { - "dss-claude-plugin": { - "path": "./dss-claude-plugin" - } - }, "permissions": { "allow": [ "mcp__zen__listmodels" ] + }, + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": [ + "dss" + ], + "enabledPlugins": { + "dss-claude-plugin@dss": true } } diff --git a/.dss/data/_system/activity/2025-12-11.jsonl b/.dss/data/_system/activity/2025-12-11.jsonl new file mode 100644 index 0000000..80eae39 --- /dev/null +++ b/.dss/data/_system/activity/2025-12-11.jsonl @@ -0,0 +1,20 @@ +{"id": "e86bf101-40a", "timestamp": "2025-12-11T07:49:51.791244", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 4, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "dc9b68b8-9f3", "timestamp": "2025-12-11T07:50:07.035285", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 4, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "0011a5dc-1b0", "timestamp": "2025-12-11T08:01:26.823275", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 53, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "4a0fe126-f37", "timestamp": "2025-12-11T08:02:35.548806", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "4d1ace92-c05", "timestamp": "2025-12-11T08:03:42.738367", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "b66abbc7-431", "timestamp": "2025-12-11T08:07:10.245195", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "a7d4564d-02a", "timestamp": "2025-12-11T08:09:31.190998", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 6, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "783d7d77-2c0", "timestamp": "2025-12-11T08:13:57.991330", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "0ba399b9-b6d", "timestamp": "2025-12-11T08:17:19.961112", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "f97422d1-e4c", "timestamp": "2025-12-11T08:18:03.166336", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "f0af79d9-65f", "timestamp": "2025-12-11T08:22:07.368109", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "1d60f899-d77", "timestamp": "2025-12-11T08:24:04.038534", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "3ed0582a-99b", "timestamp": "2025-12-11T08:28:44.464249", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 6, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "fed5cda6-db0", "timestamp": "2025-12-11T08:31:30.259400", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 6, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "2a16a9c6-af3", "timestamp": "2025-12-11T08:42:59.542999", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "d33532a0-0e0", "timestamp": "2025-12-11T08:45:32.260772", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 25, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "a1d0a7aa-317", "timestamp": "2025-12-11T09:37:40.666691", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 4, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "72a8c3f9-b7e", "timestamp": "2025-12-11T10:53:47.155433", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 4, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "2c33055d-1c3", "timestamp": "2025-12-11T10:57:07.802585", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 4, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} +{"id": "d83ddb46-96d", "timestamp": "2025-12-11T13:07:54.640979", "action": "ai_chat", "entity_type": "chat", "entity_id": "claude", "entity_name": null, "project_id": null, "user_id": null, "user_name": null, "team_context": null, "category": "other", "severity": "info", "description": "Ai Chat chat", "details": {"message_length": 4, "tools_enabled": true, "model": "claude"}, "ip_address": null, "user_agent": null} diff --git a/dss/mcp/context/__init__.py b/.dss/data/_system/activity/2025-12-11.jsonl.lock similarity index 100% rename from dss/mcp/context/__init__.py rename to .dss/data/_system/activity/2025-12-11.jsonl.lock diff --git a/.dss/dss.pid b/.dss/dss.pid new file mode 100644 index 0000000..f322bf7 --- /dev/null +++ b/.dss/dss.pid @@ -0,0 +1 @@ +61719 diff --git a/.gitignore b/.gitignore index 6a8d3e5..a35f960 100644 --- a/.gitignore +++ b/.gitignore @@ -68,5 +68,5 @@ dist/ out/ storybook-static/ venv/ -# Local MCP config (generated) -# .mcp.json +# Local MCP config (generated by ./scripts/setup-mcp.sh) +.claude/mcp.json diff --git a/.mcp.json b/.mcp.json index 8b4dbba..9484085 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,17 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/schemas/mcp-servers.schema.json", - "mcpServers": { - "dss": { - "command": "/home/overbits/dss/.venv/bin/python3", - "args": ["/home/overbits/dss/dss-claude-plugin/servers/dss-mcp-server.py"], - "env": { - "PYTHONPATH": "/home/overbits/dss:/home/overbits/dss/dss-claude-plugin", - "DSS_HOME": "/home/overbits/dss/.dss", - "DSS_DATABASE": "/home/overbits/dss/.dss/dss.db", - "DSS_CACHE": "/home/overbits/dss/.dss/cache", - "DSS_BASE_PATH": "/home/overbits/dss" - }, - "description": "Design System Server MCP - local development" - } - } + "mcpServers": {} } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a4bb694 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,124 @@ +# DSS - Design System Server + +## FIRST: Enable DSS MCP Tools + +**Before starting work, check if DSS tools are available.** If you don't see `dss_*` tools in your available MCP tools, run: + +```bash +./scripts/setup-mcp.sh +``` + +Then tell the user: "I've configured the DSS MCP server. Please restart Claude Code to load the DSS tools, then continue this conversation." + +The DSS MCP server provides 40+ design system tools for Figma sync, token management, component generation, and project analysis. + +### Manual verification + +Check if `.claude/mcp.json` exists and has valid paths: +- `command` should point to `.venv/bin/python3` (must exist) +- `args` should point to `dss-claude-plugin/servers/dss-mcp-server.py` (must exist) + +## Project Structure + +``` +dss/ +├── dss/ # Core Python library +│ ├── mcp_server/ # MCP server implementation +│ ├── analyze/ # Code analysis tools +│ ├── ingest/ # Token ingestion +│ ├── figma/ # Figma integration +│ ├── storybook/ # Storybook generation +│ └── storage/ # JSON-based storage +├── apps/ +│ ├── api/ # FastAPI server (port 6220) +│ └── cli/ # TypeScript CLI +├── admin-ui/ # Admin dashboard (port 6221) +├── dss-claude-plugin/ # Claude Code MCP plugin +│ └── servers/ # MCP server scripts +└── scripts/ # Setup & utility scripts +``` + +## Standard Ports + +| Service | Port | +|------------|------| +| API Server | 6220 | +| Admin UI | 6221 | +| MCP Server | 6222 | +| Storybook | 6226 | + +## Development Setup + +```bash +# Python environment +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +# Admin UI +cd admin-ui && npm install + +# Generate MCP config +./scripts/setup-mcp.sh +``` + +## Starting Services + +```bash +# API Server +source .venv/bin/activate +PYTHONPATH="/path/to/dss:/path/to/dss/apps/api" uvicorn apps.api.server:app --host 0.0.0.0 --port 6220 + +# Admin UI +cd admin-ui && npm run dev +``` + +## Key Files + +- `dss/mcp_server/handler.py` - MCP tool execution handler +- `dss/storage/json_store.py` - JSON-based data storage +- `apps/api/server.py` - FastAPI server +- `.claude/mcp.json` - Local MCP configuration (generated) + +## Troubleshooting MCP Connection Issues + +### DSS MCP server fails to connect + +If `/mcp` shows "Failed to reconnect to dss", check: + +1. **Virtual environment exists**: The `.venv` directory must exist with Python installed + ```bash + # If missing, create it: + python3 -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt + ``` + +2. **MCP config paths are valid**: Check `.claude/mcp.json` points to existing files: + - `.venv/bin/python3` must exist + - `dss-claude-plugin/servers/dss-mcp-server.py` must exist + +3. **Restart Claude Code** after fixing any configuration issues + +### Disabling unwanted MCP servers + +MCP servers can be configured in multiple locations. Check all of these: + +| Location | Used By | +|----------|---------| +| `~/.claude/mcp.json` | Claude Code (global) | +| `~/.config/claude/claude_desktop_config.json` | Claude Desktop app | +| `.claude/mcp.json` (project) | Claude Code (project-specific) | +| `../.mcp.json` | Parent directory inheritance | + +To disable a server, remove its entry from the relevant config file and restart Claude Code. + +### Common issue: Figma MCP errors + +If you see repeated `MCP server "figma": No token data found` errors, the figma server is likely configured in `~/.config/claude/claude_desktop_config.json`. Remove the `"figma"` entry from that file. + +## Notes + +- DSS uses JSON-based storage, not SQL database +- The `dss/mcp_server/` directory was renamed from `dss/mcp/` to avoid shadowing the pip `mcp` package +- Integration configs (Figma, Jira, etc.) are stored encrypted when database is configured diff --git a/README.md b/README.md index a9fdaae..f0628bc 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,138 @@ Monolithic design system platform. Ingest tokens from Figma/CSS/SCSS/Tailwind, n ## Quick Start ```bash +# 1. Create Python virtual environment +python3 -m venv .venv +source .venv/bin/activate pip install -r requirements.txt -python tools/api/server.py # REST API on :3456 -python tools/api/mcp_server.py # MCP server on :3457 + +# 2. Generate MCP config for Claude Code +./scripts/setup-mcp.sh + +# 3. Start services +PYTHONPATH="$PWD:$PWD/apps/api" uvicorn apps.api.server:app --host 0.0.0.0 --port 6220 ``` +## Claude Code Plugin Integration + +DSS integrates with Claude Code as a **plugin** that provides MCP tools, slash commands, skills, and agents. + +### Installation + +**Step 1: Set up the Python environment** + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +**Step 2: Run the setup script** + +```bash +./scripts/setup-mcp.sh +``` + +**Step 3: Add the DSS marketplace and install the plugin** + +In Claude Code, run: + +``` +/plugin marketplace add /path/to/dss/dss-claude-plugin +``` + +Replace `/path/to/dss` with your actual DSS installation path. + +Then install the plugin: + +``` +/plugin install dss-claude-plugin@dss +``` + +**Alternative: Manual configuration** + +Add to your `~/.claude/settings.json`: + +```json +{ + "extraKnownMarketplaces": { + "dss": { + "source": { + "source": "directory", + "path": "/path/to/dss/dss-claude-plugin" + } + } + }, + "enabledPlugins": { + "dss-claude-plugin@dss": true + } +} +``` + +**Step 4: Restart Claude Code** completely (quit and reopen) + +### Verification + +After restart, verify the plugin is loaded: + +1. Run `/mcp` - DSS server should appear in the list +2. If DSS shows as disconnected, select it to enable +3. DSS tools will be available as `dss_*` functions + +### Troubleshooting + +**Plugin not found error in debug logs?** + +The plugin must be discoverable. Ensure the path in `.claude/mcp.json` points to valid files: + +```bash +# Verify paths exist +ls -la .venv/bin/python3 +ls -la dss-claude-plugin/servers/dss-mcp-server.py +``` + +**DSS server not connecting?** + +Add DSS to your global MCP config (`~/.claude/mcp.json`): + +```json +{ + "mcpServers": { + "dss": { + "command": "/path/to/dss/.venv/bin/python3", + "args": ["/path/to/dss/dss-claude-plugin/servers/dss-mcp-server.py"], + "env": { + "PYTHONPATH": "/path/to/dss:/path/to/dss/dss-claude-plugin", + "DSS_HOME": "/path/to/dss/.dss", + "DSS_BASE_PATH": "/path/to/dss" + } + } + } +} +``` + +**Test the MCP server manually:** + +```bash +source .venv/bin/activate +PYTHONPATH="$PWD:$PWD/dss-claude-plugin" \ + python3 dss-claude-plugin/servers/dss-mcp-server.py +``` + +**Check debug logs:** + +```bash +cat ~/.claude/debug/latest | grep -i "dss\|plugin" +``` + +### Available Tools + +Once connected, DSS provides tools prefixed with `dss_`: +- `dss_figma_*` - Figma integration and token extraction +- `dss_token_*` - Design token management +- `dss_component_*` - Component generation +- `dss_project_*` - Project analysis + ## Structure ``` diff --git a/admin-ui/AI-REFERENCE.md b/admin-ui/AI-REFERENCE.md index d6884f8..b168cd8 100644 --- a/admin-ui/AI-REFERENCE.md +++ b/admin-ui/AI-REFERENCE.md @@ -27,9 +27,9 @@ admin-ui/src/ │ ├── client.ts # API client with all endpoints │ └── types.ts # TypeScript interfaces ├── components/ -│ ├── base/ # Button, Card, Input, Badge, Spinner +│ ├── base/ # Button, Card, Input, Badge, Spinner, Skeleton │ ├── layout/ # Shell, Header, Sidebar, Panel, ChatSidebar -│ └── shared/ # CommandPalette, Toast +│ └── shared/ # CommandPalette, Toast, Modal, ErrorBoundary, DataTable ├── workdesks/ # Team-specific views │ ├── UIWorkdesk.tsx # Figma extraction, code generation │ ├── UXWorkdesk.tsx # Token list, Figma files @@ -170,8 +170,10 @@ endpoints.mcp.status() // GET /api/mcp/status **Tools**: - `dashboard` - Figma connection status, file sync status -- `token-list` - View tokens from Figma - `figma-files` - Manage connected Figma files +- `token-list` - View tokens from Figma +- `asset-list` - Gallery of design assets (icons, images, illustrations) +- `component-list` - Design system components - `figma-plugin` - Plugin installation info ### QA Team (`QAWorkdesk.tsx`) @@ -179,8 +181,10 @@ endpoints.mcp.status() // GET /api/mcp/status **Tools**: - `dashboard` - Health score, ESRE definitions count +- `figma-live-compare` - QA validation: Figma vs live implementation - `esre-editor` - Create/edit/delete ESRE definitions - `console-viewer` - Browser console log capture +- `network-monitor` - Track network requests in real-time - `test-results` - View ESRE test results ### Admin (`AdminWorkdesk.tsx`) @@ -190,6 +194,7 @@ endpoints.mcp.status() // GET /api/mcp/status - `settings` - Server config, Figma token, Storybook URL - `projects` - CRUD for projects - `integrations` - External service connections +- `mcp-tools` - View and execute MCP tools for AI assistants - `audit-log` - System activity with filtering/export - `cache-management` - Clear/purge cache - `health-monitor` - Service status with auto-refresh diff --git a/admin-ui/src/App.tsx b/admin-ui/src/App.tsx index 8e281c2..baa0caa 100644 --- a/admin-ui/src/App.tsx +++ b/admin-ui/src/App.tsx @@ -4,6 +4,7 @@ import { theme, initializeApp } from './state'; import { Shell } from './components/layout/Shell'; import { CommandPalette } from './components/shared/CommandPalette'; import { ToastContainer } from './components/shared/Toast'; +import { ErrorBoundary } from './components/shared/ErrorBoundary'; import { useKeyboardShortcuts } from './hooks/useKeyboard'; export function App() { @@ -43,10 +44,10 @@ export function App() { }, []); return ( - <> + - + ); } diff --git a/admin-ui/src/components/base/Skeleton.css b/admin-ui/src/components/base/Skeleton.css new file mode 100644 index 0000000..6ae8034 --- /dev/null +++ b/admin-ui/src/components/base/Skeleton.css @@ -0,0 +1,141 @@ +/* Skeleton Loader Styles */ + +.skeleton { + background-color: var(--color-muted); + display: block; +} + +/* Variants */ +.skeleton-text { + height: 1em; + border-radius: var(--radius-sm); + margin-bottom: var(--spacing-2); +} + +.skeleton-text:last-child { + margin-bottom: 0; +} + +.skeleton-circular { + border-radius: 50%; +} + +.skeleton-rectangular { + border-radius: 0; +} + +.skeleton-rounded { + border-radius: var(--radius-md); +} + +/* Animations */ +.skeleton-pulse { + animation: skeleton-pulse 1.5s ease-in-out infinite; +} + +@keyframes skeleton-pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.4; + } + 100% { + opacity: 1; + } +} + +.skeleton-wave { + position: relative; + overflow: hidden; +} + +.skeleton-wave::after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + animation: skeleton-wave 1.5s linear infinite; +} + +@keyframes skeleton-wave { + 100% { + transform: translateX(100%); + } +} + +[data-theme="dark"] .skeleton-wave::after { + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.1), + transparent + ); +} + +/* Skeleton patterns */ +.skeleton-text-block { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +.skeleton-card { + background-color: var(--color-surface-0); + border-radius: var(--radius-lg); + overflow: hidden; + border: 1px solid var(--color-border); +} + +.skeleton-card-content { + padding: var(--spacing-4); + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +.skeleton-list-item { + display: flex; + align-items: center; + gap: var(--spacing-3); + padding: var(--spacing-3) 0; +} + +.skeleton-list-content { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--spacing-1); +} + +.skeleton-table { + display: flex; + flex-direction: column; + gap: var(--spacing-1); +} + +.skeleton-table-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: var(--spacing-4); + padding: var(--spacing-3) 0; + border-bottom: 1px solid var(--color-border); +} + +.skeleton-table-header { + padding-bottom: var(--spacing-3); + border-bottom: 2px solid var(--color-border); +} + +.skeleton-table-row:last-child { + border-bottom: none; +} diff --git a/admin-ui/src/components/base/Skeleton.tsx b/admin-ui/src/components/base/Skeleton.tsx new file mode 100644 index 0000000..111913d --- /dev/null +++ b/admin-ui/src/components/base/Skeleton.tsx @@ -0,0 +1,106 @@ +import { JSX } from 'preact'; +import './Skeleton.css'; + +export interface SkeletonProps { + variant?: 'text' | 'circular' | 'rectangular' | 'rounded'; + width?: string | number; + height?: string | number; + animation?: 'pulse' | 'wave' | 'none'; + className?: string; + style?: JSX.CSSProperties; +} + +export function Skeleton({ + variant = 'text', + width, + height, + animation = 'pulse', + className = '', + style +}: SkeletonProps) { + const classes = [ + 'skeleton', + `skeleton-${variant}`, + animation !== 'none' && `skeleton-${animation}`, + className + ].filter(Boolean).join(' '); + + const combinedStyle: JSX.CSSProperties = { + width: typeof width === 'number' ? `${width}px` : width, + height: typeof height === 'number' ? `${height}px` : height, + ...style + }; + + return
; +} + +// Predefined skeleton patterns +export function SkeletonText({ lines = 3, className = '' }: { lines?: number; className?: string }) { + return ( +
+ {Array.from({ length: lines }).map((_, i) => ( + + ))} +
+ ); +} + +export function SkeletonCard({ className = '' }: { className?: string }) { + return ( +
+ +
+ + +
+
+ ); +} + +export function SkeletonAvatar({ size = 40, className = '' }: { size?: number; className?: string }) { + return ( + + ); +} + +export function SkeletonListItem({ className = '' }: { className?: string }) { + return ( +
+ +
+ + +
+
+ ); +} + +export function SkeletonTable({ rows = 5, columns = 4, className = '' }: { rows?: number; columns?: number; className?: string }) { + return ( +
+ {/* Header */} +
+ {Array.from({ length: columns }).map((_, i) => ( + + ))} +
+ {/* Body */} + {Array.from({ length: rows }).map((_, rowIndex) => ( +
+ {Array.from({ length: columns }).map((_, colIndex) => ( + + ))} +
+ ))} +
+ ); +} diff --git a/admin-ui/src/components/shared/DataTable.css b/admin-ui/src/components/shared/DataTable.css new file mode 100644 index 0000000..bf341ee --- /dev/null +++ b/admin-ui/src/components/shared/DataTable.css @@ -0,0 +1,164 @@ +/* DataTable Styles */ + +.data-table-container { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.data-table-search { + max-width: 300px; +} + +.data-table-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-3); + padding: var(--spacing-8); + color: var(--color-muted-foreground); +} + +.data-table-wrapper { + overflow-x: auto; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-sm); +} + +.data-table th, +.data-table td { + padding: var(--spacing-3) var(--spacing-4); + text-align: left; + border-bottom: 1px solid var(--color-border); +} + +.data-table th { + background-color: var(--color-surface-1); + font-weight: var(--font-weight-medium); + color: var(--color-muted-foreground); + white-space: nowrap; +} + +.data-table th.sortable { + cursor: pointer; + user-select: none; +} + +.data-table th.sortable:hover { + background-color: var(--color-muted); +} + +.th-content { + display: flex; + align-items: center; + gap: var(--spacing-1); +} + +.sort-indicator { + font-size: var(--font-size-xs); + color: var(--color-primary); +} + +.data-table tbody tr { + transition: background-color var(--duration-fast) var(--timing-out); +} + +.data-table tbody tr:hover { + background-color: var(--color-surface-1); +} + +.data-table tbody tr.clickable { + cursor: pointer; +} + +.data-table tbody tr:last-child td { + border-bottom: none; +} + +.data-table td { + color: var(--color-foreground); +} + +.empty-message { + text-align: center; + color: var(--color-muted-foreground); + padding: var(--spacing-8) !important; +} + +.actions-column { + width: 100px; + text-align: right !important; +} + +.actions-cell { + text-align: right; + white-space: nowrap; +} + +.actions-cell > * { + margin-left: var(--spacing-1); +} + +/* Pagination */ +.data-table-pagination { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--spacing-3); +} + +.pagination-info { + font-size: var(--font-size-sm); + color: var(--color-muted-foreground); +} + +.pagination-controls { + display: flex; + align-items: center; + gap: var(--spacing-1); +} + +.page-indicator { + padding: 0 var(--spacing-3); + font-size: var(--font-size-sm); + color: var(--color-muted-foreground); +} + +/* Code in cells */ +.data-table td code { + padding: var(--spacing-1) var(--spacing-2); + background-color: var(--color-surface-1); + border-radius: var(--radius-sm); + font-size: var(--font-size-xs); + font-family: var(--font-mono); +} + +/* Status badges in cells */ +.data-table td .badge { + margin: 0; +} + +/* Responsive */ +@media (max-width: 640px) { + .data-table th, + .data-table td { + padding: var(--spacing-2) var(--spacing-3); + } + + .data-table-pagination { + flex-direction: column; + align-items: stretch; + } + + .pagination-controls { + justify-content: center; + } +} diff --git a/admin-ui/src/components/shared/DataTable.tsx b/admin-ui/src/components/shared/DataTable.tsx new file mode 100644 index 0000000..3b0b644 --- /dev/null +++ b/admin-ui/src/components/shared/DataTable.tsx @@ -0,0 +1,249 @@ +import { ComponentChildren } from 'preact'; +import { useState, useMemo } from 'preact/hooks'; +import { Spinner } from '../base/Spinner'; +import { Input } from '../base/Input'; +import { Button } from '../base/Button'; +import './DataTable.css'; + +export interface Column { + key: string; + header: string; + width?: string; + sortable?: boolean; + render?: (value: unknown, row: T, index: number) => ComponentChildren; +} + +export interface DataTableProps> { + columns: Column[]; + data: T[]; + loading?: boolean; + emptyMessage?: string; + searchable?: boolean; + searchKeys?: string[]; + sortable?: boolean; + pagination?: boolean; + pageSize?: number; + onRowClick?: (row: T, index: number) => void; + rowClassName?: (row: T, index: number) => string; + actions?: (row: T, index: number) => ComponentChildren; +} + +type SortDirection = 'asc' | 'desc' | null; + +export function DataTable>({ + columns, + data, + loading = false, + emptyMessage = 'No data available', + searchable = false, + searchKeys = [], + sortable = true, + pagination = true, + pageSize = 10, + onRowClick, + rowClassName, + actions +}: DataTableProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [sortColumn, setSortColumn] = useState(null); + const [sortDirection, setSortDirection] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + // Filter data based on search + const filteredData = useMemo(() => { + if (!searchTerm || searchKeys.length === 0) return data; + + const term = searchTerm.toLowerCase(); + return data.filter(row => + searchKeys.some(key => { + const value = row[key]; + return value && String(value).toLowerCase().includes(term); + }) + ); + }, [data, searchTerm, searchKeys]); + + // Sort data + const sortedData = useMemo(() => { + if (!sortColumn || !sortDirection) return filteredData; + + return [...filteredData].sort((a, b) => { + const aVal = a[sortColumn]; + const bVal = b[sortColumn]; + + if (aVal === bVal) return 0; + if (aVal === null || aVal === undefined) return 1; + if (bVal === null || bVal === undefined) return -1; + + const comparison = String(aVal).localeCompare(String(bVal), undefined, { numeric: true }); + return sortDirection === 'asc' ? comparison : -comparison; + }); + }, [filteredData, sortColumn, sortDirection]); + + // Paginate data + const paginatedData = useMemo(() => { + if (!pagination) return sortedData; + + const start = (currentPage - 1) * pageSize; + return sortedData.slice(start, start + pageSize); + }, [sortedData, currentPage, pageSize, pagination]); + + const totalPages = Math.ceil(sortedData.length / pageSize); + + // Handle sort toggle + function handleSort(columnKey: string) { + if (!sortable) return; + + const column = columns.find(c => c.key === columnKey); + if (column?.sortable === false) return; + + if (sortColumn === columnKey) { + if (sortDirection === 'asc') { + setSortDirection('desc'); + } else if (sortDirection === 'desc') { + setSortColumn(null); + setSortDirection(null); + } + } else { + setSortColumn(columnKey); + setSortDirection('asc'); + } + } + + // Reset to page 1 when search changes + const handleSearch = (value: string) => { + setSearchTerm(value); + setCurrentPage(1); + }; + + if (loading) { + return ( +
+ + Loading data... +
+ ); + } + + return ( +
+ {searchable && ( +
+ handleSearch((e.target as HTMLInputElement).value)} + size="sm" + /> +
+ )} + +
+ + + + {columns.map(column => ( + + ))} + {actions && } + + + + {paginatedData.length === 0 ? ( + + + + ) : ( + paginatedData.map((row, index) => { + const actualIndex = (currentPage - 1) * pageSize + index; + return ( + onRowClick?.(row, actualIndex)} + > + {columns.map(column => ( + + ))} + {actions && ( + + )} + + ); + }) + )} + +
sortable && column.sortable !== false && handleSort(column.key)} + > + + {column.header} + {sortColumn === column.key && ( + + {sortDirection === 'asc' ? ' \u2191' : ' \u2193'} + + )} + + Actions
+ {emptyMessage} +
+ {column.render + ? column.render(row[column.key], row, actualIndex) + : String(row[column.key] ?? '-')} + + {actions(row, actualIndex)} +
+
+ + {pagination && totalPages > 1 && ( +
+ + Showing {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, sortedData.length)} of {sortedData.length} + +
+ + + + {currentPage} / {totalPages} + + + +
+
+ )} +
+ ); +} diff --git a/admin-ui/src/components/shared/ErrorBoundary.css b/admin-ui/src/components/shared/ErrorBoundary.css new file mode 100644 index 0000000..c6c5595 --- /dev/null +++ b/admin-ui/src/components/shared/ErrorBoundary.css @@ -0,0 +1,63 @@ +/* Error Boundary Styles */ + +.error-boundary { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + padding: var(--spacing-6); +} + +.error-boundary .card { + max-width: 500px; + width: 100%; +} + +.error-details { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.error-message { + padding: var(--spacing-3); + background-color: var(--color-error-bg); + border: 1px solid var(--color-error); + border-radius: var(--radius-md); + color: var(--color-error); + font-family: var(--font-mono); + font-size: var(--font-size-sm); + word-break: break-word; +} + +.error-stack { + margin-top: var(--spacing-2); +} + +.error-stack summary { + cursor: pointer; + font-size: var(--font-size-sm); + color: var(--color-muted-foreground); + padding: var(--spacing-2) 0; +} + +.error-stack summary:hover { + color: var(--color-foreground); +} + +.error-stack pre { + margin-top: var(--spacing-2); + padding: var(--spacing-3); + background-color: var(--color-surface-1); + border-radius: var(--radius-md); + font-size: var(--font-size-xs); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; +} + +.error-boundary .card-footer { + gap: var(--spacing-3); +} diff --git a/admin-ui/src/components/shared/ErrorBoundary.tsx b/admin-ui/src/components/shared/ErrorBoundary.tsx new file mode 100644 index 0000000..574496e --- /dev/null +++ b/admin-ui/src/components/shared/ErrorBoundary.tsx @@ -0,0 +1,104 @@ +import { Component, ComponentChildren } from 'preact'; +import { Button } from '../base/Button'; +import { Card, CardHeader, CardContent, CardFooter } from '../base/Card'; +import './ErrorBoundary.css'; + +interface ErrorBoundaryProps { + children: ComponentChildren; + fallback?: ComponentChildren; + onError?: (error: Error, errorInfo: { componentStack: string }) => void; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: { componentStack: string } | null; +} + +export class ErrorBoundary extends Component { + state: ErrorBoundaryState = { + hasError: false, + error: null, + errorInfo: null + }; + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: { componentStack: string }) { + this.setState({ errorInfo }); + + // Log error to console + console.error('ErrorBoundary caught an error:', error, errorInfo); + + // Call optional error handler + this.props.onError?.(error, errorInfo); + } + + handleReset = () => { + this.setState({ hasError: false, error: null, errorInfo: null }); + }; + + handleReload = () => { + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + // Custom fallback if provided + if (this.props.fallback) { + return this.props.fallback; + } + + // Default error UI + return ( +
+ + + +
+

+ {this.state.error?.message || 'Unknown error'} +

+ {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( +
+ Stack trace +
{this.state.errorInfo.componentStack}
+
+ )} +
+
+ + + + +
+
+ ); + } + + return this.props.children; + } +} + +// Hook-based wrapper for functional components +export function withErrorBoundary

( + WrappedComponent: (props: P) => ComponentChildren, + fallback?: ComponentChildren +) { + return function WithErrorBoundary(props: P) { + return ( + + + + ); + }; +} diff --git a/admin-ui/src/components/shared/Modal.css b/admin-ui/src/components/shared/Modal.css new file mode 100644 index 0000000..8e06aa7 --- /dev/null +++ b/admin-ui/src/components/shared/Modal.css @@ -0,0 +1,165 @@ +/* Modal Styles */ + +.modal-overlay { + position: fixed; + inset: 0; + z-index: var(--z-50); + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-4); + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + animation: fadeIn var(--duration-fast) var(--timing-out); +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modal { + display: flex; + flex-direction: column; + background-color: var(--color-surface-0); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + max-height: calc(100vh - var(--spacing-8)); + animation: slideUp var(--duration-normal) var(--timing-out); +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Modal sizes */ +.modal-sm { + width: 100%; + max-width: 400px; +} + +.modal-md { + width: 100%; + max-width: 560px; +} + +.modal-lg { + width: 100%; + max-width: 720px; +} + +.modal-xl { + width: 100%; + max-width: 960px; +} + +.modal-full { + width: calc(100vw - var(--spacing-8)); + max-width: none; + height: calc(100vh - var(--spacing-8)); +} + +/* Modal header */ +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-4) var(--spacing-6); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +.modal-title { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-foreground); +} + +.modal-close { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + margin: calc(var(--spacing-1) * -1); + background: transparent; + border: none; + border-radius: var(--radius-md); + color: var(--color-muted-foreground); + cursor: pointer; + transition: all var(--duration-fast) var(--timing-out); +} + +.modal-close:hover { + background-color: var(--color-muted); + color: var(--color-foreground); +} + +.modal-close:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Modal content */ +.modal-content { + flex: 1; + padding: var(--spacing-6); + overflow-y: auto; +} + +/* Modal footer */ +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-3); + padding: var(--spacing-4) var(--spacing-6); + border-top: 1px solid var(--color-border); + flex-shrink: 0; +} + +/* Confirm dialog specific */ +.confirm-message { + margin: 0; + color: var(--color-muted-foreground); + line-height: var(--line-height-relaxed); +} + +/* Dark mode adjustments */ +[data-theme="dark"] .modal-overlay { + background-color: rgba(0, 0, 0, 0.7); +} + +/* Responsive */ +@media (max-width: 640px) { + .modal-overlay { + padding: var(--spacing-2); + align-items: flex-end; + } + + .modal { + max-height: calc(100vh - var(--spacing-4)); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + .modal-sm, + .modal-md, + .modal-lg, + .modal-xl { + max-width: none; + } +} diff --git a/admin-ui/src/components/shared/Modal.tsx b/admin-ui/src/components/shared/Modal.tsx new file mode 100644 index 0000000..f778472 --- /dev/null +++ b/admin-ui/src/components/shared/Modal.tsx @@ -0,0 +1,152 @@ +import { JSX, ComponentChildren } from 'preact'; +import { useEffect, useCallback } from 'preact/hooks'; +import { signal } from '@preact/signals'; +import { Button } from '../base/Button'; +import './Modal.css'; + +// Global modal state +export const modalOpen = signal(false); +export const modalContent = signal(null); +export const modalTitle = signal(''); + +export interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: ComponentChildren; + size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; + closeOnOverlayClick?: boolean; + closeOnEscape?: boolean; + showCloseButton?: boolean; + footer?: ComponentChildren; +} + +export function Modal({ + isOpen, + onClose, + title, + children, + size = 'md', + closeOnOverlayClick = true, + closeOnEscape = true, + showCloseButton = true, + footer +}: ModalProps) { + // Handle escape key + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape' && closeOnEscape) { + onClose(); + } + }, [closeOnEscape, onClose]); + + // Handle overlay click + const handleOverlayClick = useCallback((e: MouseEvent) => { + if (closeOnOverlayClick && e.target === e.currentTarget) { + onClose(); + } + }, [closeOnOverlayClick, onClose]); + + // Add/remove event listeners and body scroll lock + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = ''; + }; + }, [isOpen, handleKeyDown]); + + if (!isOpen) return null; + + return ( +

}> +
+ {(title || showCloseButton) && ( +
+ {title && } + {showCloseButton && ( + + )} +
+ )} +
+ {children} +
+ {footer && ( +
+ {footer} +
+ )} +
+
+ ); +} + +// Confirm dialog helper +export interface ConfirmDialogProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + variant?: 'danger' | 'warning' | 'default'; + loading?: boolean; +} + +export function ConfirmDialog({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = 'Confirm', + cancelText = 'Cancel', + variant = 'default', + loading = false +}: ConfirmDialogProps) { + return ( + + + + + } + > +

{message}

+
+ ); +} + +// Hook for managing modal state +export function useModal() { + const isOpen = signal(false); + + const open = () => { isOpen.value = true; }; + const close = () => { isOpen.value = false; }; + const toggle = () => { isOpen.value = !isOpen.value; }; + + return { isOpen: isOpen.value, open, close, toggle }; +} diff --git a/admin-ui/src/state/team.ts b/admin-ui/src/state/team.ts index 1083c69..3acc38a 100644 --- a/admin-ui/src/state/team.ts +++ b/admin-ui/src/state/team.ts @@ -50,11 +50,12 @@ export const TEAM_CONFIGS: Record = { description: 'Design consistency & token validation', tools: [ { id: 'dashboard', name: 'Dashboard', description: 'Team metrics and quick actions' }, + { id: 'live-canvas', name: 'Live Canvas', description: 'AI-powered component generator - your own Figma Make' }, + { id: 'figma-files', name: 'Figma Files', description: 'Manage connected Figma files' }, { id: 'figma-plugin', name: 'Figma Plugin', description: 'Export tokens/assets/components from Figma' }, { id: 'token-list', name: 'Token List', description: 'View all design tokens' }, { id: 'asset-list', name: 'Asset List', description: 'Gallery of design assets' }, - { id: 'component-list', name: 'Component List', description: 'Design system components' }, - { id: 'navigation-demos', name: 'Navigation Demos', description: 'Generate navigation flow demos' } + { id: 'component-list', name: 'Component List', description: 'Design system components' } ], panels: ['metrics', 'diff', 'accessibility', 'screenshots', 'chat'], metrics: ['figmaFiles', 'syncedFiles', 'pendingSync', 'designTokens'], @@ -69,8 +70,8 @@ export const TEAM_CONFIGS: Record = { { id: 'figma-live-compare', name: 'Figma vs Live', description: 'QA validation: Figma design vs live implementation' }, { id: 'esre-editor', name: 'ESRE Editor', description: 'Edit Explicit Style Requirements and Expectations' }, { id: 'console-viewer', name: 'Console Viewer', description: 'Monitor browser console logs', mcpTool: 'browser_get_logs' }, - { id: 'network-monitor', name: 'Network Monitor', description: 'Track network requests', mcpTool: 'devtools_network_requests' }, - { id: 'error-tracker', name: 'Error Tracker', description: 'Track uncaught exceptions', mcpTool: 'browser_get_errors' } + { id: 'network-monitor', name: 'Network Monitor', description: 'Track network requests in real-time' }, + { id: 'test-results', name: 'Test Results', description: 'View ESRE test results and history' } ], panels: ['metrics', 'console', 'network', 'tests', 'chat'], metrics: ['healthScore', 'esreDefinitions', 'testsRun', 'testsPassed'], @@ -84,6 +85,7 @@ export const TEAM_CONFIGS: Record = { { id: 'settings', name: 'System Settings', description: 'Configure DSS hostname, port, and setup type' }, { id: 'projects', name: 'Projects', description: 'Create and manage design system projects' }, { id: 'integrations', name: 'Integrations', description: 'Configure Figma, Jira, and other integrations' }, + { id: 'mcp-tools', name: 'MCP Tools', description: 'View and execute MCP tools for AI assistants' }, { id: 'audit-log', name: 'Audit Log', description: 'View all system activity' }, { id: 'cache-management', name: 'Cache Management', description: 'Clear and manage system cache' }, { id: 'health-monitor', name: 'Health Monitor', description: 'System health dashboard' }, diff --git a/admin-ui/src/workdesks/AdminWorkdesk.tsx b/admin-ui/src/workdesks/AdminWorkdesk.tsx index de260d5..e56dbfe 100644 --- a/admin-ui/src/workdesks/AdminWorkdesk.tsx +++ b/admin-ui/src/workdesks/AdminWorkdesk.tsx @@ -6,7 +6,7 @@ import { Badge } from '../components/base/Badge'; import { Input, Select } from '../components/base/Input'; import { Spinner } from '../components/base/Spinner'; import { endpoints } from '../api/client'; -import type { Project, RuntimeConfig, AuditEntry, SystemHealth, Service } from '../api/types'; +import type { Project, RuntimeConfig, AuditEntry, SystemHealth, Service, MCPTool } from '../api/types'; import './Workdesk.css'; interface AdminWorkdeskProps { @@ -21,6 +21,7 @@ export default function AdminWorkdesk({ activeTool }: AdminWorkdeskProps) { const toolViews: Record = { 'projects': , 'integrations': , + 'mcp-tools': , 'audit-log': , 'cache-management': , 'health-monitor': , @@ -448,6 +449,173 @@ function IntegrationsTool() { ); } +function MCPToolsTool() { + const [tools, setTools] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedTool, setSelectedTool] = useState(null); + const [executing, setExecuting] = useState(false); + const [params, setParams] = useState>({}); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [mcpStatus, setMcpStatus] = useState<{ connected: boolean; tools: number } | null>(null); + + useEffect(() => { + loadTools(); + }, []); + + async function loadTools() { + setLoading(true); + try { + const [toolsResult, statusResult] = await Promise.allSettled([ + endpoints.mcp.tools(), + endpoints.mcp.status() + ]); + + if (toolsResult.status === 'fulfilled') { + setTools(toolsResult.value); + } + if (statusResult.status === 'fulfilled') { + setMcpStatus(statusResult.value); + } + } catch (err) { + console.error('Failed to load MCP tools:', err); + } finally { + setLoading(false); + } + } + + async function handleExecute() { + if (!selectedTool) return; + + setExecuting(true); + setError(null); + setResult(null); + + try { + const response = await endpoints.mcp.execute(selectedTool.name, params); + setResult(response); + } catch (err) { + setError(err instanceof Error ? err.message : 'Execution failed'); + } finally { + setExecuting(false); + } + } + + function handleSelectTool(tool: MCPTool) { + setSelectedTool(tool); + setParams({}); + setResult(null); + setError(null); + } + + if (loading) { + return ( +
+
+ + Loading MCP tools... +
+
+ ); + } + + return ( +
+
+

MCP Tools

+

Model Context Protocol tools for AI assistants

+
+ + {/* MCP Status */} + {mcpStatus && ( +
+ + MCP: {mcpStatus.connected ? 'Connected' : 'Not Connected'} + + {mcpStatus.tools} tools available +
+ )} + + {/* Tools List */} + + Refresh} + /> + + {tools.length === 0 ? ( +

No MCP tools available

+ ) : ( +
+ {tools.map(tool => ( +
handleSelectTool(tool)} + > +
+ {tool.name} + {tool.description} +
+ {tool.category && {tool.category}} +
+ ))} +
+ )} +
+
+ + {/* Tool Execution */} + {selectedTool && ( + + + +
+ {Object.entries(selectedTool.input_schema.properties || {}).map(([key, prop]) => ( + setParams(p => ({ ...p, [key]: (e.target as HTMLInputElement).value }))} + placeholder={prop.description || `Enter ${key}`} + hint={prop.type} + fullWidth + /> + ))} +
+ {error && ( +
+ {error} +
+ )} +
+ + + +
+ )} + + {/* Results */} + {result && ( + + + +
+              {JSON.stringify(result, null, 2)}
+            
+
+
+ )} +
+ ); +} + function AuditLogTool() { const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); diff --git a/admin-ui/src/workdesks/QAWorkdesk.tsx b/admin-ui/src/workdesks/QAWorkdesk.tsx index 8fda242..223b5a1 100644 --- a/admin-ui/src/workdesks/QAWorkdesk.tsx +++ b/admin-ui/src/workdesks/QAWorkdesk.tsx @@ -22,6 +22,7 @@ export default function QAWorkdesk({ activeTool }: QAWorkdeskProps) { const toolViews: Record = { 'esre-editor': , 'console-viewer': , + 'network-monitor': , 'figma-live-compare': , 'test-results': , }; @@ -532,6 +533,147 @@ function ConsoleViewerTool() { ); } +function NetworkMonitorTool() { + const [requests, setRequests] = useState>([]); + const [filter, setFilter] = useState('all'); + const [isCapturing, setIsCapturing] = useState(false); + + useEffect(() => { + if (!isCapturing) return; + + // Intercept fetch requests + const originalFetch = window.fetch; + window.fetch = async function(...args) { + const startTime = performance.now(); + const url = typeof args[0] === 'string' ? args[0] : (args[0] as Request).url; + const method = typeof args[0] === 'string' ? (args[1]?.method || 'GET') : (args[0] as Request).method || 'GET'; + + const requestId = Date.now(); + + // Add pending request + setRequests(prev => [...prev.slice(-99), { + id: requestId, + method: method.toUpperCase(), + url, + status: null, + duration: null, + size: null, + type: 'fetch', + timestamp: new Date().toLocaleTimeString() + }]); + + try { + const response = await originalFetch.apply(this, args); + const duration = Math.round(performance.now() - startTime); + const contentLength = response.headers.get('content-length'); + + // Update with response data + setRequests(prev => prev.map(r => + r.id === requestId + ? { ...r, status: response.status, duration, size: contentLength ? `${Math.round(parseInt(contentLength) / 1024)}KB` : null } + : r + )); + + return response; + } catch (error) { + setRequests(prev => prev.map(r => + r.id === requestId + ? { ...r, status: 0, duration: Math.round(performance.now() - startTime) } + : r + )); + throw error; + } + }; + + return () => { + window.fetch = originalFetch; + }; + }, [isCapturing]); + + const filteredRequests = filter === 'all' + ? requests + : requests.filter(r => { + if (filter === 'success') return r.status !== null && r.status >= 200 && r.status < 400; + if (filter === 'error') return r.status === null || r.status === 0 || r.status >= 400; + return true; + }); + + return ( +
+
+

Network Monitor

+

Track network requests in real-time

+
+ + + + setSearchTerm((e.target as HTMLInputElement).value)} + /> + +
+ } + /> + + {filteredAssets.length === 0 ? ( +

No assets found

+ ) : ( +
+ {filteredAssets.map(asset => ( +
+
+ {asset.type === 'icon' ? '\u25A1' : asset.type === 'image' ? '\u25A3' : '\u25A2'} +
+
+ {asset.name} + {asset.format} | {asset.size} +
+ {asset.type} +
+ ))} +
+ )} +
+ +
+ ); +} + +function ComponentListTool() { + const [components, setComponents] = useState>([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadComponents(); + }, []); + + async function loadComponents() { + setLoading(true); + try { + const projectId = currentProject.value?.id; + if (projectId) { + const result = await endpoints.projects.components(projectId); + setComponents(result.map(c => ({ + id: c.id, + name: c.display_name || c.name, + description: c.description, + variants: c.variants?.length || 0, + status: (c as unknown as { status?: string }).status === 'active' ? 'ready' : 'in-progress' + }))); + } + } catch (err) { + console.error('Failed to load components:', err); + setComponents([]); + } finally { + setLoading(false); + } + } + + if (loading) { + return ( +
+
+ + Loading components... +
+
+ ); + } + + return ( +
+
+

Component List

+

Design system components

+
+ + + Refresh} + /> + + {components.length === 0 ? ( +

No components found. Extract components from Figma first.

+ ) : ( +
+ {components.map(component => ( +
+
+ {component.name} + {component.description && ( + {component.description} + )} +
+
+ {component.variants && component.variants > 0 && ( + {component.variants} variants + )} + + {component.status} + +
+
+ ))} +
+ )} +
+
+
+ ); +} + function FigmaPluginTool() { return (
@@ -523,6 +748,317 @@ function FigmaPluginTool() { ); } +interface GeneratedComponent { + id: string; + prompt: string; + code: string; + timestamp: number; +} + +function LiveCanvasTool() { + const [prompt, setPrompt] = useState(''); + const [isGenerating, setIsGenerating] = useState(false); + const [generatedCode, setGeneratedCode] = useState(null); + const [error, setError] = useState(null); + const [history, setHistory] = useState([]); + const [viewport, setViewport] = useState<'desktop' | 'tablet' | 'mobile'>('desktop'); + const iframeRef = useRef(null); + + const viewportSizes = { + desktop: { width: '100%', maxWidth: '1200px' }, + tablet: { width: '768px', maxWidth: '768px' }, + mobile: { width: '375px', maxWidth: '375px' } + }; + + async function handleGenerate() { + if (!prompt.trim()) return; + + setIsGenerating(true); + setError(null); + + try { + // Build the context for Claude + const context = { + team: 'ux', + request: 'generate_ui_component', + project: currentProject.value ? { + id: currentProject.value.id, + name: currentProject.value.name + } : null + }; + + // Create a detailed prompt for component generation + const componentPrompt = `Generate a React/Preact component for the following request. Return ONLY the JSX code that can be rendered directly in an iframe. Do not include imports or exports. Use inline styles or standard CSS class names. Make it visually complete and polished. + +Request: ${prompt} + +Requirements: +- Use modern, clean design principles +- Include proper spacing and typography +- Use a cohesive color scheme (prefer neutral/professional colors) +- Make it responsive +- Return ONLY the JSX code, nothing else`; + + const response = await endpoints.claude.chat(componentPrompt, currentProject.value?.id, context); + + if (response.message?.content) { + // Extract code from the response + let code = response.message.content; + + // Try to extract code from markdown code blocks if present + const codeBlockMatch = code.match(/```(?:jsx?|tsx?|html)?\s*([\s\S]*?)```/); + if (codeBlockMatch) { + code = codeBlockMatch[1].trim(); + } + + setGeneratedCode(code); + + // Add to history + const newComponent: GeneratedComponent = { + id: `gen-${Date.now()}`, + prompt: prompt, + code: code, + timestamp: Date.now() + }; + setHistory(prev => [newComponent, ...prev.slice(0, 9)]); + + // Render in iframe + renderInIframe(code); + } else { + setError('No response from Claude. Check your API key configuration.'); + } + } catch (err) { + console.error('Generation failed:', err); + setError(err instanceof Error ? err.message : 'Failed to generate component'); + } finally { + setIsGenerating(false); + } + } + + function renderInIframe(code: string) { + const iframe = iframeRef.current; + if (!iframe) return; + + // Create the HTML document to render in the iframe + const htmlContent = ` + + + + + + + + + + + +
+ + +`; + + // Write to iframe + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if (doc) { + doc.open(); + doc.write(htmlContent); + doc.close(); + } + } + + function handleLoadFromHistory(item: GeneratedComponent) { + setPrompt(item.prompt); + setGeneratedCode(item.code); + renderInIframe(item.code); + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleGenerate(); + } + } + + function handleRefresh() { + if (generatedCode) { + renderInIframe(generatedCode); + } + } + + function handleCopyCode() { + if (generatedCode) { + navigator.clipboard.writeText(generatedCode); + } + } + + return ( +
+
+

Live Canvas

+

Generate UI components with AI - your own Figma Make

+
+ + {/* Prompt Input Area */} + + + +
+