Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions packages/website/MOCK_SETUP.md
Original file line number Diff line number Diff line change
@@ -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";`<br>`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
26 changes: 26 additions & 0 deletions packages/website/app/api/mock/check-visual-edit/route.ts
Original file line number Diff line number Diff line change
@@ -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() }
)
}
59 changes: 59 additions & 0 deletions packages/website/app/api/mock/visual-edit/route.ts
Original file line number Diff line number Diff line change
@@ -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() }
)
}
}
5 changes: 5 additions & 0 deletions packages/website/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ const RootLayout = ({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<script
dangerouslySetInnerHTML={{
__html: `window.USE_MOCKS = ${process.env.USE_MOCKS === "true"};`,
}}
/>
<NuqsAdapter>{children}</NuqsAdapter>
<Analytics />
</body>
Expand Down
9 changes: 8 additions & 1 deletion packages/website/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { init } from "react-grab/core";
declare global {
interface Window {
__REACT_GRAB__?: ReturnType<typeof init>;
USE_MOCKS?: boolean;
}
}

Expand All @@ -18,6 +19,12 @@ const isUserInAbusiveRegion = (): boolean => {
}
};

const getVisualEditEndpoint = () => {
const useMocks = typeof window !== "undefined" &&
(window as Window & { USE_MOCKS?: boolean }).USE_MOCKS;
return useMocks ? "/api/mock/visual-edit" : "/api/visual-edit";
};

if (typeof window !== "undefined" && !window.__REACT_GRAB__) {
const api = init({
onActivate: () => {
Expand All @@ -31,7 +38,7 @@ if (typeof window !== "undefined" && !window.__REACT_GRAB__) {
// HACK: Temporarily disable visual edit for abusive regions
if (!isUserInAbusiveRegion()) {
const { provider, getOptions, onStart, onComplete, onUndo } =
createVisualEditAgentProvider({ apiEndpoint: "/api/visual-edit" });
createVisualEditAgentProvider({ apiEndpoint: getVisualEditEndpoint() });

api.setAgent({
provider,
Expand Down
17 changes: 17 additions & 0 deletions packages/website/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface EnvironmentConfig {
useMocks: boolean
visualEditEndpoint: string
visualEditHealthCheckEndpoint: string
}

const getEnvironmentConfig = (): EnvironmentConfig => {
const useMocks = process.env.USE_MOCKS === "true"

return {
useMocks,
visualEditEndpoint: useMocks ? "/api/mock/visual-edit" : "/api/visual-edit",
visualEditHealthCheckEndpoint: useMocks ? "/api/mock/check-visual-edit" : "/api/check-visual-edit",
}
}

export const env = getEnvironmentConfig()
Loading