diff --git a/packages/website/MOCK_SETUP.md b/packages/website/MOCK_SETUP.md new file mode 100644 index 000000000..ad2e7e6da --- /dev/null +++ b/packages/website/MOCK_SETUP.md @@ -0,0 +1,220 @@ +# Visual Edit API Mock Infrastructure + +## Overview + +The Visual Edit API mock infrastructure enables local development and testing without requiring external API dependencies. It provides pattern-matched, predictable responses for common DOM manipulation requests. + +## Setup + +### Environment Configuration + +Mock mode is controlled by the `USE_MOCKS` environment variable. + +### Running in Mock Mode + +```bash +# Using the provided script +nr dev:mock + +# Or manually +USE_MOCKS=true nr dev +``` + +### Running in Production Mode + +```bash +# Normal development (uses real Cerebras API) +nr dev + +# Build (always uses real API) +nr build +``` + +## Architecture + +### Core Components + +1. **Environment Config** (`lib/env.ts`) + - Centralized environment variable management + - Dynamic endpoint selection based on `USE_MOCKS` + +2. **Mock State Manager** (`lib/mock-state/visual-edit-mock.ts`) + - Singleton pattern for global state management + - Pattern-matching engine for DOM manipulation requests + - Request counting for testing + +3. **Mock API Endpoints** + - `/api/mock/visual-edit` - Main code generation endpoint + - `/api/mock/check-visual-edit` - Health check endpoint + - Both return 403 if `USE_MOCKS=false` + +4. **Client Integration** (`instrumentation-client.ts`) + - Dynamic endpoint selection + - Window-scoped `USE_MOCKS` flag + +## Supported Mock Patterns + +The mock state manager recognizes these patterns and generates appropriate JavaScript: + +| Pattern Keywords | Example Prompt | Generated Code | +|-----------------|----------------|----------------| +| `change text`, `update text`, `set text` | "Change text to 'Hello World'" | `element.textContent = "Hello World";` | +| `add class`, `toggle class` | "Add class 'active'" | `element.classList.add("active");` | +| `remove class`, `delete class` | "Remove class 'hidden'" | `element.classList.remove("hidden");` | +| `change color`, `set color` | "Change color to #ff0000" | `element.style.color = "#ff0000";` | +| `background` | "Set background to blue" | `element.style.backgroundColor = "blue";` | +| `hide`, `remove` | "Hide this element" | `element.style.display = "none";` | +| `show`, `reveal`, `display` | "Show the element" | `element.style.display = "block";` | +| `fade`, `opacity` | "Fade to 0.5 opacity" | `element.style.opacity = "0.5";` | +| `animate`, `transition` | "Animate this element" | `element.style.transition = "all 0.3s ease";`
`element.style.transform = "scale(1.05)";` | +| `border` | "Add red border" | `element.style.border = "2px solid red";` | +| `width`, `height`, `size` | "Set width to 200px" | `element.style.width = "200px";` | + +### Fallback Behavior + +If no pattern matches, the mock returns: +```javascript +element.style.outline = "2px solid #3b82f6"; +``` + +## API Contract + +### POST /api/mock/visual-edit + +**Request:** +```json +{ + "messages": [ + { + "role": "user", + "content": "Change the text to 'Hello World'" + } + ] +} +``` + +**Response:** +```javascript +element.textContent = "Hello World"; +``` + +**Headers:** +- `Content-Type: text/javascript` +- `Cache-Control: no-cache, no-store, must-revalidate` + +**Constraints:** +- Maximum 15,000 characters per message +- 300-500ms simulated delay for realism +- Returns 403 if `USE_MOCKS=false` + +### GET /api/mock/check-visual-edit + +**Response:** +```json +{ + "healthy": true +} +``` + +## Testing + +### Manual Test Checklist + +Run these tests with `USE_MOCKS=true`: + +- [ ] **Environment Toggle** + - [ ] Verify `USE_MOCKS=false` returns 403 from mock endpoints + - [ ] Verify `USE_MOCKS=true` enables mock endpoints + +- [ ] **Code Generation** + - [ ] Text change: "Change text to 'Test'" + - [ ] Add class: "Add class 'active'" + - [ ] Remove class: "Remove class 'hidden'" + - [ ] Color change: "Change color to red" + - [ ] Background: "Set background to blue" + - [ ] Hide element: "Hide this" + - [ ] Show element: "Show this" + - [ ] Opacity: "Set opacity to 0.5" + - [ ] Animation: "Animate this element" + - [ ] Border: "Add green border" + - [ ] Size: "Set width to 300px" + +- [ ] **Integration** + - [ ] No external API calls made in mock mode + - [ ] Generated code executes without errors + - [ ] Visual changes apply correctly to DOM + +- [ ] **Error Handling** + - [ ] Message exceeding 15,000 characters returns 400 + - [ ] Invalid JSON returns 500 + - [ ] CORS headers present on all responses + +### Programmatic Testing + +```typescript +import { visualEditMockState } from "./lib/mock-state/visual-edit-mock" + +// Reset state between tests +visualEditMockState.reset() + +// Generate code +const code = visualEditMockState.generateCode("Change text to 'Test'") +console.log(code) // element.textContent = "Test"; + +// Check request count +console.log(visualEditMockState.getRequestCount()) // 1 +``` + +### Mock Reset Utility + +The mock state manager provides a `reset()` method for test isolation: + +```typescript +// Reset request count and state +visualEditMockState.reset() +``` + +## Limitations + +- Mock responses are pattern-matched, not AI-generated +- Complex multi-step manipulations return single-step code +- No natural language understanding beyond keyword matching +- No context awareness across multiple requests + +## Production Behavior + +When `USE_MOCKS=false` (production): +- All requests route to `/api/visual-edit` (real Cerebras API) +- Mock endpoints return 403 +- No mock state tracking +- Full AI-powered code generation + +## Troubleshooting + +### Mock endpoints return 403 +- Verify `USE_MOCKS=true` environment variable +- Check that script is injected in `app/layout.tsx` +- Confirm window.USE_MOCKS is set in browser console + +### Generated code doesn't match pattern +- Review supported patterns table +- Check that prompt includes recognized keywords +- Fallback behavior applies for unrecognized patterns + +### No visual changes occur +- Verify React Grab is initialized +- Check browser console for JavaScript errors +- Confirm element selection is correct + +## Future Enhancements + +Potential improvements to the mock infrastructure: + +- [ ] Enhanced pattern matching with regex support +- [ ] Multi-step code generation for complex requests +- [ ] Mock analytics and usage tracking +- [ ] UI for inspecting mock state +- [ ] Configurable delay ranges +- [ ] Pattern customization via config file +- [ ] Request/response logging +- [ ] Mock data persistence \ No newline at end of file diff --git a/packages/website/app/api/mock/check-visual-edit/route.ts b/packages/website/app/api/mock/check-visual-edit/route.ts new file mode 100644 index 000000000..d9cd669d1 --- /dev/null +++ b/packages/website/app/api/mock/check-visual-edit/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server" +import { env } from "../../../../lib/env" + +const getCorsHeaders = () => ({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +}) + +export const OPTIONS = async () => { + return NextResponse.json({}, { headers: getCorsHeaders() }) +} + +export const GET = async () => { + if (!env.useMocks) { + return NextResponse.json( + { error: "Mock endpoints are disabled. Set USE_MOCKS=true to enable." }, + { status: 403, headers: getCorsHeaders() } + ) + } + + return NextResponse.json( + { healthy: true }, + { headers: getCorsHeaders() } + ) +} \ No newline at end of file diff --git a/packages/website/app/api/mock/visual-edit/route.ts b/packages/website/app/api/mock/visual-edit/route.ts new file mode 100644 index 000000000..04879aca7 --- /dev/null +++ b/packages/website/app/api/mock/visual-edit/route.ts @@ -0,0 +1,59 @@ +import { NextRequest, NextResponse } from "next/server" +import { env } from "../../../../lib/env" +import { visualEditMockState } from "../../../../lib/mock-state/visual-edit-mock" + +const MAX_INPUT_CHARACTERS = 15000 + +const getCorsHeaders = () => ({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}) + +export const OPTIONS = async () => { + return NextResponse.json({}, { headers: getCorsHeaders() }) +} + +export const POST = async (request: NextRequest) => { + if (!env.useMocks) { + return NextResponse.json( + { error: "Mock endpoints are disabled. Set USE_MOCKS=true to enable." }, + { status: 403, headers: getCorsHeaders() } + ) + } + + try { + const body = await request.json() + const messages = body.messages || [] + + for (const message of messages) { + if (message.content.length > MAX_INPUT_CHARACTERS) { + return NextResponse.json( + { error: `Message exceeds maximum length of ${MAX_INPUT_CHARACTERS} characters` }, + { status: 400, headers: getCorsHeaders() } + ) + } + } + + const lastMessage = messages[messages.length - 1] + const prompt = lastMessage?.content || "" + + await new Promise((resolve) => setTimeout(resolve, 300 + Math.random() * 200)) + + const generatedCode = visualEditMockState.generateCode(prompt) + + return new NextResponse(generatedCode, { + headers: { + ...getCorsHeaders(), + "Content-Type": "text/javascript", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + }) + } catch (error) { + console.error("Mock visual-edit error:", error) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: getCorsHeaders() } + ) + } +} \ No newline at end of file diff --git a/packages/website/app/layout.tsx b/packages/website/app/layout.tsx index 4c3c7b75a..93c999f1f 100644 --- a/packages/website/app/layout.tsx +++ b/packages/website/app/layout.tsx @@ -56,6 +56,11 @@ const RootLayout = ({ +