Skip to content

[Solution]: Developing an AI Designer Assistant. #156

Description

@kstij1

AI Designer Assistant – Complete Build and Integration Guide

Part 1: Building the Standalone AI Designer Assistant

What You're Building

The AI Designer Assistant is a web application that enables users to edit designs using natural language commands. Users upload design files (images, Figma exports, or connect live Figma files) and then issue commands like "Change all headers to blue" or "Align buttons to the right". The app processes these commands, applies visual transformations, and provides an interactive preview with undo/redo functionality.

Core Features

  • Design Upload: Support for PNG, JPG, SVG, and Figma file connections
  • Natural Language Commands: Process text commands to modify design elements
  • Interactive Preview: Real-time visualization of design changes
  • Undo/Redo System: Track and revert design operations
  • Element Selection: Click to select and modify specific design elements
  • Export Options: Save modified designs as PNG, PDF, or SVG
  • Design History: Save and restore design sessions

Tech Stack

Frontend:

  • Next.js 14 with App Router
  • TypeScript for type safety
  • Tailwind CSS for styling
  • Fabric.js for canvas manipulation
  • React DnD for drag-and-drop functionality
  • React Hook Form for form management

Backend:

  • Next.js API routes
  • OpenAI GPT-4 for command parsing
  • Sharp for image processing
  • Figma API for live file integration
  • MongoDB for session storage

Development Tools:

  • ESLint and Prettier for code quality
  • Jest for testing
  • Docker for containerization

Project Structure

designer-assistant/
├── app/
│   ├── (auth)/
│   │   └── login/
│   ├── (dashboard)/
│   │   ├── designer/
│   │   │   ├── page.tsx
│   │   │   ├── new/
│   │   │   │   └── page.tsx
│   │   │   └── [id]/
│   │   │       └── page.tsx
│   │   └── layout.tsx
│   ├── api/
│   │   ├── designs/
│   │   │   ├── route.ts
│   │   │   ├── [id]/
│   │   │   │   └── route.ts
│   │   │   └── upload/
│   │   │       └── route.ts
│   │   ├── transform/
│   │   │   └── route.ts
│   │   ├── export/
│   │   │   └── route.ts
│   │   └── figma/
│   │       └── route.ts
│   ├── globals.css
│   └── layout.tsx
├── components/
│   ├── designer/
│   │   ├── DesignUploader.tsx
│   │   ├── CommandInterface.tsx
│   │   ├── DesignCanvas.tsx
│   │   ├── HistoryPanel.tsx
│   │   ├── ExportOptions.tsx
│   │   └── ElementInspector.tsx
│   ├── ui/
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Textarea.tsx
│   │   ├── Modal.tsx
│   │   └── FileUpload.tsx
│   └── layout/
│       ├── Header.tsx
│       └── Sidebar.tsx
├── lib/
│   ├── ai/
│   │   └── commandParser.ts
│   ├── canvas/
│   │   ├── fabricUtils.ts
│   │   └── transformations.ts
│   ├── export/
│   │   ├── imageExport.ts
│   │   └── pdfExport.ts
│   ├── figma/
│   │   └── figmaApi.ts
│   ├── database/
│   │   └── mongodb.ts
│   └── utils.ts
├── types/
│   └── designer.ts
├── public/
│   └── icons/
├── package.json
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
└── .env.local

Data Models

// types/designer.ts
export interface DesignElement {
  id: string;
  type: 'text' | 'image' | 'shape' | 'button' | 'container';
  properties: {
    x: number;
    y: number;
    width: number;
    height: number;
    rotation?: number;
    opacity?: number;
    color?: string;
    backgroundColor?: string;
    fontSize?: number;
    fontFamily?: string;
    text?: string;
    src?: string;
  };
  styles: {
    border?: string;
    borderRadius?: number;
    boxShadow?: string;
    zIndex?: number;
  };
}

export interface DesignSession {
  id: string;
  name: string;
  sourceType: 'upload' | 'figma';
  sourceUrl?: string;
  elements: DesignElement[];
  operations: DesignOperation[];
  currentOperation: number;
  createdAt: Date;
  updatedAt: Date;
  userId: string;
}

export interface DesignOperation {
  id: string;
  type: 'color_change' | 'position_change' | 'size_change' | 'text_change' | 'style_change';
  target: string | string[]; // element IDs
  command: string;
  before: any;
  after: any;
  timestamp: Date;
}

export interface TransformRequest {
  command: string;
  targetElements?: string[];
  options?: {
    color?: string;
    position?: { x?: number; y?: number };
    size?: { width?: number; height?: number };
    style?: any;
  };
}

API Endpoints

// app/api/designs/route.ts
POST   /api/designs           // Create new design session
GET    /api/designs           // List user's design sessions

// app/api/designs/[id]/route.ts
GET    /api/designs/[id]      // Get specific design session
PUT    /api/designs/[id]      // Update design session
DELETE /api/designs/[id]      // Delete design session

// app/api/designs/upload/route.ts
POST   /api/designs/upload    // Upload design file

// app/api/transform/route.ts
POST   /api/transform         // Apply design transformation

// app/api/export/route.ts
POST   /api/export            // Export design to various formats

// app/api/figma/route.ts
POST   /api/figma/connect     // Connect Figma file
GET    /api/figma/file        // Get Figma file data

Core Components

1. Design Uploader Component

// components/designer/DesignUploader.tsx
'use client';

import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { Button } from '@/components/ui/Button';

interface DesignUploaderProps {
  onUpload: (file: File) => void;
  onFigmaConnect: (url: string) => void;
  loading?: boolean;
}

export default function DesignUploader({ onUpload, onFigmaConnect, loading }: DesignUploaderProps) {
  const [figmaUrl, setFigmaUrl] = useState('');
  const [uploadType, setUploadType] = useState<'file' | 'figma'>('file');

  const onDrop = useCallback((acceptedFiles: File[]) => {
    if (acceptedFiles.length > 0) {
      onUpload(acceptedFiles[0]);
    }
  }, [onUpload]);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      'image/*': ['.png', '.jpg', '.jpeg', '.svg'],
      'application/json': ['.fig']
    },
    multiple: false
  });

  const handleFigmaConnect = () => {
    if (figmaUrl.trim()) {
      onFigmaConnect(figmaUrl);
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">AI Designer Assistant</h1>
      
      <div className="mb-6">
        <div className="flex space-x-4 mb-4">
          <button
            onClick={() => setUploadType('file')}
            className={`px-4 py-2 rounded ${
              uploadType === 'file' ? 'bg-blue-600 text-white' : 'bg-gray-200'
            }`}
          >
            Upload File
          </button>
          <button
            onClick={() => setUploadType('figma')}
            className={`px-4 py-2 rounded ${
              uploadType === 'figma' ? 'bg-blue-600 text-white' : 'bg-gray-200'
            }`}
          >
            Connect Figma
          </button>
        </div>
      </div>

      {uploadType === 'file' ? (
        <div
          {...getRootProps()}
          className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer ${
            isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
          }`}
        >
          <input {...getInputProps()} />
          <div className="space-y-4">
            <div className="text-4xl">📁</div>
            <div>
              <p className="text-lg font-medium">
                {isDragActive ? 'Drop your design file here' : 'Drag & drop your design file'}
              </p>
              <p className="text-gray-500">or click to browse</p>
            </div>
            <div className="text-sm text-gray-400">
              Supports PNG, JPG, SVG, and Figma exports
            </div>
          </div>
        </div>
      ) : (
        <div className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-2">
              Figma File URL
            </label>
            <input
              type="url"
              value={figmaUrl}
              onChange={(e) => setFigmaUrl(e.target.value)}
              placeholder="https://www.figma.com/file/..."
              className="w-full p-3 border rounded-lg"
            />
          </div>
          <Button
            onClick={handleFigmaConnect}
            disabled={loading || !figmaUrl.trim()}
            className="w-full"
          >
            {loading ? 'Connecting...' : 'Connect Figma File'}
          </Button>
        </div>
      )}
    </div>
  );
}

2. Command Interface Component

// components/designer/CommandInterface.tsx
'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Textarea } from '@/components/ui/Textarea';

interface CommandInterfaceProps {
  onCommand: (command: string) => void;
  loading?: boolean;
  suggestions?: string[];
}

export default function CommandInterface({ onCommand, loading, suggestions }: CommandInterfaceProps) {
  const [command, setCommand] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!command.trim()) return;
    
    onCommand(command);
    setCommand('');
  };

  const handleSuggestionClick = (suggestion: string) => {
    setCommand(suggestion);
  };

  return (
    <div className="bg-white border rounded-lg p-4">
      <h3 className="font-semibold mb-4">Design Commands</h3>
      
      <form onSubmit={handleSubmit} className="space-y-4">
        <Textarea
          value={command}
          onChange={(e) => setCommand(e.target.value)}
          placeholder="Describe what you want to change... (e.g., 'Change all headers to blue', 'Align buttons to the right')"
          rows={3}
          className="w-full"
        />
        
        <div className="flex justify-between items-center">
          <Button
            type="submit"
            disabled={loading || !command.trim()}
            size="sm"
          >
            {loading ? 'Processing...' : 'Apply Command'}
          </Button>
          
          <div className="text-sm text-gray-500">
            Try natural language commands
          </div>
        </div>
      </form>
      
      {suggestions && suggestions.length > 0 && (
        <div className="mt-4">
          <h4 className="text-sm font-medium mb-2">Suggestions:</h4>
          <div className="flex flex-wrap gap-2">
            {suggestions.map((suggestion, index) => (
              <button
                key={index}
                onClick={() => handleSuggestionClick(suggestion)}
                className="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded"
              >
                {suggestion}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

3. Design Canvas Component

// components/designer/DesignCanvas.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import { fabric } from 'fabric';
import { DesignElement } from '@/types/designer';

interface DesignCanvasProps {
  elements: DesignElement[];
  onElementSelect: (elementId: string) => void;
  onElementUpdate: (elementId: string, updates: Partial<DesignElement>) => void;
  selectedElementId?: string;
}

export default function DesignCanvas({ 
  elements, 
  onElementSelect, 
  onElementUpdate, 
  selectedElementId 
}: DesignCanvasProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const fabricCanvasRef = useRef<fabric.Canvas | null>(null);
  const [canvasSize, setCanvasSize] = useState({ width: 800, height: 600 });

  useEffect(() => {
    if (!canvasRef.current) return;

    const canvas = new fabric.Canvas(canvasRef.current, {
      width: canvasSize.width,
      height: canvasSize.height,
      backgroundColor: '#ffffff'
    });

    fabricCanvasRef.current = canvas;

    // Handle object selection
    canvas.on('selection:created', (e) => {
      const activeObject = e.selected?.[0];
      if (activeObject && activeObject.data?.elementId) {
        onElementSelect(activeObject.data.elementId);
      }
    });

    // Handle object modification
    canvas.on('object:modified', (e) => {
      const object = e.target;
      if (object && object.data?.elementId) {
        const updates = {
          properties: {
            x: object.left || 0,
            y: object.top || 0,
            width: object.width || 0,
            height: object.height || 0,
            rotation: object.angle || 0,
            opacity: object.opacity || 1
          }
        };
        onElementUpdate(object.data.elementId, updates);
      }
    });

    return () => {
      canvas.dispose();
    };
  }, [canvasSize, onElementSelect, onElementUpdate]);

  useEffect(() => {
    if (!fabricCanvasRef.current) return;

    const canvas = fabricCanvasRef.current;
    canvas.clear();

    elements.forEach(element => {
      let fabricObject: fabric.Object;

      switch (element.type) {
        case 'text':
          fabricObject = new fabric.Text(element.properties.text || '', {
            left: element.properties.x,
            top: element.properties.y,
            fontSize: element.properties.fontSize || 16,
            fontFamily: element.properties.fontFamily || 'Arial',
            fill: element.properties.color || '#000000'
          });
          break;
        
        case 'image':
          fabric.Image.fromURL(element.properties.src || '', (img) => {
            img.set({
              left: element.properties.x,
              top: element.properties.y,
              scaleX: element.properties.width / (img.width || 1),
              scaleY: element.properties.height / (img.height || 1)
            });
            img.data = { elementId: element.id };
            canvas.add(img);
          });
          return;
        
        case 'shape':
          fabricObject = new fabric.Rect({
            left: element.properties.x,
            top: element.properties.y,
            width: element.properties.width,
            height: element.properties.height,
            fill: element.properties.backgroundColor || '#cccccc',
            stroke: element.styles.border || 'none'
          });
          break;
        
        default:
          return;
      }

      fabricObject.data = { elementId: element.id };
      canvas.add(fabricObject);
    });

    canvas.renderAll();
  }, [elements]);

  useEffect(() => {
    if (!fabricCanvasRef.current) return;

    const canvas = fabricCanvasRef.current;
    const objects = canvas.getObjects();
    
    objects.forEach(obj => {
      if (obj.data?.elementId === selectedElementId) {
        canvas.setActiveObject(obj);
      }
    });
  }, [selectedElementId]);

  return (
    <div className="flex-1 bg-gray-100 p-4">
      <div className="bg-white rounded-lg shadow-sm p-4 inline-block">
        <canvas ref={canvasRef} />
      </div>
    </div>
  );
}

4. AI Command Parser

// lib/ai/commandParser.ts
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export async function parseCommand(
  command: string, 
  elements: DesignElement[]
): Promise<TransformRequest> {
  const systemPrompt = `You are a design assistant that parses natural language commands into design transformations.

Available elements: ${elements.map(el => `${el.id} (${el.type})`).join(', ')}

Return a JSON object with this structure:
{
  "type": "color_change|position_change|size_change|text_change|style_change",
  "target": ["element_id1", "element_id2"],
  "command": "original command",
  "options": {
    "color": "#hexcolor",
    "position": {"x": number, "y": number},
    "size": {"width": number, "height": number},
    "style": {...}
  }
}

Guidelines:
- Identify which elements the command targets
- Convert natural language to specific property changes
- Use appropriate transformation types
- Provide valid color hex codes
- Use pixel values for positions and sizes`;

  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: command }
    ],
    temperature: 0.3,
  });

  const content = response.choices[0].message.content;
  return JSON.parse(content || '{}');
}

5. Design Transformations

// lib/canvas/transformations.ts
import { DesignElement, DesignOperation } from '@/types/designer';

export function applyTransformation(
  elements: DesignElement[],
  transform: TransformRequest
): { elements: DesignElement[]; operation: DesignOperation } {
  const updatedElements = [...elements];
  const beforeStates: any = {};
  const afterStates: any = {};

  transform.target?.forEach(elementId => {
    const elementIndex = updatedElements.findIndex(el => el.id === elementId);
    if (elementIndex === -1) return;

    const element = updatedElements[elementIndex];
    beforeStates[elementId] = { ...element };

    // Apply transformation based on type
    switch (transform.type) {
      case 'color_change':
        if (transform.options?.color) {
          updatedElements[elementIndex] = {
            ...element,
            properties: {
              ...element.properties,
              color: transform.options.color
            }
          };
        }
        break;
      
      case 'position_change':
        if (transform.options?.position) {
          updatedElements[elementIndex] = {
            ...element,
            properties: {
              ...element.properties,
              x: transform.options.position.x ?? element.properties.x,
              y: transform.options.position.y ?? element.properties.y
            }
          };
        }
        break;
      
      case 'size_change':
        if (transform.options?.size) {
          updatedElements[elementIndex] = {
            ...element,
            properties: {
              ...element.properties,
              width: transform.options.size.width ?? element.properties.width,
              height: transform.options.size.height ?? element.properties.height
            }
          };
        }
        break;
      
      case 'text_change':
        if (transform.options?.text) {
          updatedElements[elementIndex] = {
            ...element,
            properties: {
              ...element.properties,
              text: transform.options.text
            }
          };
        }
        break;
    }

    afterStates[elementId] = { ...updatedElements[elementIndex] };
  });

  const operation: DesignOperation = {
    id: crypto.randomUUID(),
    type: transform.type,
    target: transform.target || [],
    command: transform.command,
    before: beforeStates,
    after: afterStates,
    timestamp: new Date()
  };

  return { elements: updatedElements, operation };
}

6. History Panel Component

// components/designer/HistoryPanel.tsx
'use client';

import { DesignOperation } from '@/types/designer';

interface HistoryPanelProps {
  operations: DesignOperation[];
  currentOperation: number;
  onUndo: () => void;
  onRedo: () => void;
  onJumpToOperation: (index: number) => void;
}

export default function HistoryPanel({ 
  operations, 
  currentOperation, 
  onUndo, 
  onRedo, 
  onJumpToOperation 
}: HistoryPanelProps) {
  return (
    <div className="w-64 bg-white border-l p-4">
      <div className="flex items-center justify-between mb-4">
        <h3 className="font-semibold">History</h3>
        <div className="flex space-x-2">
          <button
            onClick={onUndo}
            disabled={currentOperation <= 0}
            className="px-2 py-1 text-sm bg-gray-200 rounded disabled:opacity-50"
          >
            Undo
          </button>
          <button
            onClick={onRedo}
            disabled={currentOperation >= operations.length - 1}
            className="px-2 py-1 text-sm bg-gray-200 rounded disabled:opacity-50"
          >
            Redo
          </button>
        </div>
      </div>
      
      <div className="space-y-2">
        {operations.map((operation, index) => (
          <div
            key={operation.id}
            className={`p-2 rounded cursor-pointer text-sm ${
              index === currentOperation 
                ? 'bg-blue-100 border border-blue-300' 
                : 'bg-gray-50 hover:bg-gray-100'
            }`}
            onClick={() => onJumpToOperation(index)}
          >
            <div className="font-medium">{operation.command}</div>
            <div className="text-xs text-gray-500">
              {operation.timestamp.toLocaleTimeString()}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Main Application Page

// app/(dashboard)/designer/page.tsx
'use client';

import { useState, useEffect } from 'react';
import { DesignSession, DesignElement, DesignOperation } from '@/types/designer';
import DesignUploader from '@/components/designer/DesignUploader';
import CommandInterface from '@/components/designer/CommandInterface';
import DesignCanvas from '@/components/designer/DesignCanvas';
import HistoryPanel from '@/components/designer/HistoryPanel';
import ExportOptions from '@/components/designer/ExportOptions';

export default function DesignerPage() {
  const [sessions, setSessions] = useState<DesignSession[]>([]);
  const [currentSession, setCurrentSession] = useState<DesignSession | null>(null);
  const [selectedElementId, setSelectedElementId] = useState<string | undefined>();
  const [loading, setLoading] = useState(false);
  const [showExport, setShowExport] = useState(false);

  const handleFileUpload = async (file: File) => {
    setLoading(true);
    try {
      const formData = new FormData();
      formData.append('file', file);
      
      const response = await fetch('/api/designs/upload', {
        method: 'POST',
        body: formData,
      });
      
      const { elements } = await response.json();
      
      const newSession: DesignSession = {
        id: crypto.randomUUID(),
        name: file.name,
        sourceType: 'upload',
        elements,
        operations: [],
        currentOperation: -1,
        createdAt: new Date(),
        updatedAt: new Date(),
        userId: 'current-user-id'
      };
      
      setCurrentSession(newSession);
    } catch (error) {
      console.error('Failed to upload file:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleFigmaConnect = async (url: string) => {
    setLoading(true);
    try {
      const response = await fetch('/api/figma/connect', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url }),
      });
      
      const { elements } = await response.json();
      
      const newSession: DesignSession = {
        id: crypto.randomUUID(),
        name: 'Figma Design',
        sourceType: 'figma',
        sourceUrl: url,
        elements,
        operations: [],
        currentOperation: -1,
        createdAt: new Date(),
        updatedAt: new Date(),
        userId: 'current-user-id'
      };
      
      setCurrentSession(newSession);
    } catch (error) {
      console.error('Failed to connect Figma:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleCommand = async (command: string) => {
    if (!currentSession) return;
    
    setLoading(true);
    try {
      const response = await fetch('/api/transform', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
          command, 
          elements: currentSession.elements 
        }),
      });
      
      const { elements, operation } = await response.json();
      
      const newOperations = currentSession.operations.slice(0, currentSession.currentOperation + 1);
      newOperations.push(operation);
      
      setCurrentSession({
        ...currentSession,
        elements,
        operations: newOperations,
        currentOperation: newOperations.length - 1,
        updatedAt: new Date()
      });
    } catch (error) {
      console.error('Failed to apply command:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleUndo = () => {
    if (!currentSession || currentSession.currentOperation <= 0) return;
    
    const newCurrentOperation = currentSession.currentOperation - 1;
    const operation = currentSession.operations[newCurrentOperation];
    
    // Restore previous state
    const restoredElements = currentSession.elements.map(element => {
      if (operation.before[element.id]) {
        return operation.before[element.id];
      }
      return element;
    });
    
    setCurrentSession({
      ...currentSession,
      elements: restoredElements,
      currentOperation: newCurrentOperation
    });
  };

  const handleRedo = () => {
    if (!currentSession || currentSession.currentOperation >= currentSession.operations.length - 1) return;
    
    const newCurrentOperation = currentSession.currentOperation + 1;
    const operation = currentSession.operations[newCurrentOperation];
    
    // Apply operation
    const updatedElements = currentSession.elements.map(element => {
      if (operation.after[element.id]) {
        return operation.after[element.id];
      }
      return element;
    });
    
    setCurrentSession({
      ...currentSession,
      elements: updatedElements,
      currentOperation: newCurrentOperation
    });
  };

  const handleJumpToOperation = (index: number) => {
    if (!currentSession) return;
    
    // Replay operations up to the selected index
    let elements = currentSession.operations[0]?.before || currentSession.elements;
    
    for (let i = 0; i <= index; i++) {
      const operation = currentSession.operations[i];
      if (operation) {
        elements = elements.map(element => {
          if (operation.after[element.id]) {
            return operation.after[element.id];
          }
          return element;
        });
      }
    }
    
    setCurrentSession({
      ...currentSession,
      elements,
      currentOperation: index
    });
  };

  const handleElementSelect = (elementId: string) => {
    setSelectedElementId(elementId);
  };

  const handleElementUpdate = (elementId: string, updates: Partial<DesignElement>) => {
    if (!currentSession) return;
    
    const updatedElements = currentSession.elements.map(element =>
      element.id === elementId ? { ...element, ...updates } : element
    );
    
    setCurrentSession({
      ...currentSession,
      elements: updatedElements,
      updatedAt: new Date()
    });
  };

  if (!currentSession) {
    return (
      <DesignUploader
        onUpload={handleFileUpload}
        onFigmaConnect={handleFigmaConnect}
        loading={loading}
      />
    );
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <div className="flex h-screen">
        {/* Main Canvas Area */}
        <div className="flex-1 flex flex-col">
          {/* Command Interface */}
          <div className="bg-white border-b p-4">
            <CommandInterface
              onCommand={handleCommand}
              loading={loading}
              suggestions={[
                'Change all headers to blue',
                'Align buttons to the right',
                'Make text larger',
                'Add border to images'
              ]}
            />
          </div>
          
          {/* Canvas */}
          <DesignCanvas
            elements={currentSession.elements}
            onElementSelect={handleElementSelect}
            onElementUpdate={handleElementUpdate}
            selectedElementId={selectedElementId}
          />
        </div>
        
        {/* History Panel */}
        <HistoryPanel
          operations={currentSession.operations}
          currentOperation={currentSession.currentOperation}
          onUndo={handleUndo}
          onRedo={handleRedo}
          onJumpToOperation={handleJumpToOperation}
        />
      </div>
      
      {/* Export Modal */}
      {showExport && currentSession && (
        <ExportOptions
          session={currentSession}
          onClose={() => setShowExport(false)}
        />
      )}
    </div>
  );
}

Environment Configuration

# .env.local
MONGODB_URI=mongodb://localhost:27017/designer_assistant
OPENAI_API_KEY=your_openai_api_key_here
FIGMA_ACCESS_TOKEN=your_figma_access_token_here
NEXTAUTH_SECRET=your_nextauth_secret_here
NEXTAUTH_URL=http://localhost:3000

Package Dependencies

{
  "dependencies": {
    "next": "14.0.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "5.0.0",
    "tailwindcss": "3.3.0",
    "openai": "4.0.0",
    "fabric": "5.3.0",
    "sharp": "0.32.0",
    "mongodb": "6.0.0",
    "react-dropzone": "14.2.0",
    "react-hook-form": "7.45.0",
    "@hookform/resolvers": "3.3.0",
    "zod": "3.22.0"
  },
  "devDependencies": {
    "@types/node": "20.0.0",
    "@types/react": "18.2.0",
    "@types/react-dom": "18.2.0",
    "@types/fabric": "5.3.0",
    "eslint": "8.50.0",
    "eslint-config-next": "14.0.0",
    "prettier": "3.0.0",
    "jest": "29.7.0",
    "@testing-library/react": "13.4.0"
  }
}

Part 2: Now that your app is working fine, let's integrate it with Weam

Integration Overview

Once your AI Designer Assistant is fully functional as a standalone application, you can integrate it into the Weam ecosystem. This integration involves:

  1. Authentication: Using Weam's session management
  2. Database: Migrating to Weam's MongoDB with proper naming conventions
  3. UI Theming: Matching Weam's design system
  4. Routing: Setting up base path configuration
  5. Deployment: Configuring NGINX and Docker

Weam Integration Steps

Step 1: Authentication Integration

Replace your standalone authentication with Weam's iron-session:

// lib/session.ts
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';

export interface SessionData {
  user?: {
    id: string;
    email: string;
    name: string;
  };
}

export const sessionOptions = {
  cookieName: 'weam_session',
  password: process.env.IRON_SESSION_PASSWORD!,
  cookieOptions: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'lax' as const,
    path: '/',
    domain: process.env.COOKIE_DOMAIN,
    maxAge: 60 * 60 * 24 * 7, // 7 days
  },
};

export async function getSession() {
  const cookieStore = await cookies();
  return getIronSession<SessionData>(cookieStore, sessionOptions);
}

Step 2: Database Migration

Update your database collections to follow Weam's naming convention:

// lib/database/mongodb.ts
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGODB_URI!);
export const db = client.db('weam');

// Weam naming convention: solution_{solutionName}_{tableName}
export const sessions = db.collection('solution_designer_sessions');
export const exports = db.collection('solution_designer_exports');
export const templates = db.collection('solution_designer_templates');

Step 3: Base Path Configuration

Update your Next.js configuration for Weam routing:

// next.config.js
const nextConfig = {
  basePath: '/designer',
  assetPrefix: '/designer',
  async rewrites() {
    return [
      {
        source: '/designer/api/:path*',
        destination: '/api/:path*',
      },
    ];
  },
};

module.exports = nextConfig;

Step 4: Environment Variables

Update your environment configuration:

# .env.local
NEXT_PUBLIC_API_BASE_PATH=/designer
MONGODB_URI=mongodb://localhost:27017/weam
IRON_SESSION_PASSWORD=your-secure-session-password
COOKIE_DOMAIN=.weam.ai
OPENAI_API_KEY=your-openai-key
FIGMA_ACCESS_TOKEN=your-figma-token

Step 5: Sidebar Integration

Add your solution to Weam's sidebar:

// Update src/seeders/superSolution.json
{
  "name": "AI Designer Assistant",
  "path": "/designer",
  "icon": "DesignIcon",
  "description": "Edit designs using natural language commands"
}

Step 6: NGINX Configuration

Add routing configuration to Weam's NGINX:

# Add to /etc/nginx/sites-available/weam
location /designer/ {
    proxy_pass http://localhost:3012/designer/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Step 7: Docker Configuration

Create Docker setup for deployment:

# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine AS production
WORKDIR /app

COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 3012

CMD ["npm", "start"]
# docker-compose.yml
version: '3.8'

services:
  designer-assistant:
    build: .
    ports:
      - "3012:3012"
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_API_BASE_PATH=/designer
      - MONGODB_URI=${MONGODB_URI}
      - IRON_SESSION_PASSWORD=${IRON_SESSION_PASSWORD}
      - COOKIE_DOMAIN=.weam.ai
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - FIGMA_ACCESS_TOKEN=${FIGMA_ACCESS_TOKEN}
    restart: unless-stopped

Final Integration Checklist

✅ Standalone app fully functional
✅ Weam authentication integrated
✅ Database collections follow naming convention
✅ Base path configuration set
✅ Sidebar entry added
✅ NGINX routing configured
✅ Docker deployment ready
✅ Environment variables updated

Testing Integration

  1. Authentication Test: Verify users can access the app through Weam login
  2. Database Test: Confirm data is stored with proper user/company scoping
  3. Routing Test: Check that all routes work under /designer base path
  4. Command Test: Verify AI command parsing works in production
  5. Export Test: Confirm image/PDF export functionality works
  6. UI Test: Ensure theming matches Weam's design system

Your AI Designer Assistant is now fully integrated into the Weam ecosystem while maintaining all its standalone functionality!

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions