AI-driven 2D-to-3D jewelry generation with real-time customization
JewelAI is a web application that lets users describe a ring in natural language, have an AI chatbot interrogate their requirements (without assuming any detail), then generate and render a fully interactive 3D model with real-time customization controls.
The MVP covers rings only. Users can customize metal type, gemstone type/size, band width, and ring style — and see updates reflected instantly in a 3D viewer supporting front, top, side, and bottom perspectives.
- Framework: Next.js 14 (App Router) + TypeScript
- Styling: Tailwind CSS (utility-first, no CSS modules)
- 3D Viewer: Three.js (via
@react-three/fiber+@react-three/drei) - State: Zustand for global ring config state
- Deploy: Vercel
- Framework: FastAPI + Python 3.11+
- 3D Generation: TripoSG (HuggingFace) or InstantMesh (OSS fallback)
- Component Recognition: GPT-4o Vision API (OpenAI)
- AI Chatbot: Claude API — model
claude-sonnet-4-5 - File Formats: GLB (primary render), STL (export only)
- Deploy: Render (free tier)
ANTHROPIC_API_KEY= # Claude API — chatbot
OPENAI_API_KEY= # GPT-4o Vision — component recognition
HUGGINGFACE_API_TOKEN= # TripoSG inference
NEXT_PUBLIC_API_URL= # FastAPI backend URL (Render)
jewel-ai/
├── frontend/ # Next.js 14 app
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx # Main designer page
│ │ ├── api/ # Next.js API routes (thin proxies only)
│ │ └── globals.css
│ ├── components/
│ │ ├── viewer/
│ │ │ ├── RingViewer.tsx # Three.js canvas wrapper
│ │ │ ├── RingModel.tsx # Procedural / GLB mesh renderer
│ │ │ └── ViewControls.tsx # Front/Top/Side/Bottom snap buttons
│ │ ├── chat/
│ │ │ ├── ChatPanel.tsx # AI chat UI
│ │ │ ├── MessageList.tsx
│ │ │ └── ChatInput.tsx
│ │ ├── controls/
│ │ │ ├── MetalPicker.tsx
│ │ │ ├── GemPicker.tsx
│ │ │ ├── SizeSlider.tsx
│ │ │ └── StyleSelect.tsx
│ │ └── ui/ # Shared primitives (Button, Badge, etc.)
│ ├── store/
│ │ └── ringStore.ts # Zustand: metal, gem, gemSize, style, bandW
│ ├── lib/
│ │ ├── api.ts # Typed fetch wrappers → FastAPI
│ │ └── types.ts # RingConfig, ChatMessage, etc.
│ ├── public/
│ │ └── models/ # Pre-generated GLB files (static fallback)
│ ├── .env.local # NEVER commit
│ └── package.json
│
├── backend/ # FastAPI app
│ ├── main.py # App entry point + CORS
│ ├── routers/
│ │ ├── chat.py # POST /chat — Claude API streaming
│ │ ├── generate.py # POST /generate — TripoSG 3D generation
│ │ ├── recognize.py # POST /recognize — GPT-4o Vision
│ │ └── export.py # GET /export/stl — GLB → STL conversion
│ ├── services/
│ │ ├── claude_service.py # Claude API client + system prompt
│ │ ├── tripo_service.py # TripoSG / HuggingFace client
│ │ ├── vision_service.py # GPT-4o Vision client
│ │ └── mesh_service.py # trimesh GLB/STL utilities
│ ├── models/
│ │ └── schemas.py # Pydantic: RingConfig, ChatRequest, etc.
│ ├── requirements.txt
│ └── .env # NEVER commit
│
├── CLAUDE.md # ← this file
└── README.md
cd frontend
npm install
npm run dev # http://localhost:3000
npm run build
npm run lint
npm run type-check # tsc --noEmitcd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload # http://localhost:8000# Terminal 1
cd backend && uvicorn main:app --reload
# Terminal 2
cd frontend && npm run devinterface RingConfig {
style: 'solitaire' | 'halo' | 'eternity' | 'cluster' | 'split' | 'pave';
metal: 'gold' | 'silver' | 'platinum' | 'rose-gold' | 'blackened';
gem: 'diamond' | 'ruby' | 'emerald' | 'sapphire' | 'amethyst' | 'topaz' | 'morganite' | 'none';
gemSize: number; // 0.3 (tiny) → 1.0 (medium) → 2.2 (statement)
bandWidth: number; // 0.07 (thin) → 0.32 (bold)
bandProfile: 'flat' | 'semi-flat' | 'round' | 'high-dome' | 'knife-edge';
engraving?: string; // future
ringSize?: number; // US ring size, future
}interface ChatMessage {
role: 'user' | 'assistant';
content: string;
timestamp: number;
configSnapshot?: Partial<RingConfig>; // emitted when AI confirms a spec
}The Claude chatbot must never assume any design detail. It asks exactly 1–2 focused questions per turn and only confirms a spec when the user explicitly states it.
When all required fields are confirmed, Claude emits a structured config block:
<RING_CONFIG>{"metal":"gold","gem":"diamond","gemSize":1.2,"style":"solitaire","bandWidth":0.14,"bandProfile":"round"}</RING_CONFIG>
The frontend parses this tag from the streamed response and applies it to the Zustand store in real-time.
- Ring style
- Metal type
- Gemstone type
- Gemstone size (relative: tiny / small / medium / large / statement)
- Band width (thin / medium / thick)
- Band profile (optional but ask)
Always use: claude-sonnet-4-5
Use the Anthropic streaming API. Frontend reads the stream chunk-by-chunk, renders text in real-time, and parses <RING_CONFIG> when the stream closes.
| Button | Camera position | Notes |
|---|---|---|
| Free Rotate | Default | Auto-rotates; drag to orbit |
| Front | Eye-level, face-on | Ring torus visible |
| Top | Directly above | Stone setting prominent |
| Side | 90° rotation | Profile / band thickness |
| Bottom | Below ring | Underside of setting |
- Mouse drag / touch drag: orbit rotation (lerp smoothed)
- Scroll / pinch: zoom in/out (min 2.0, max 10.0 camera Z)
- View buttons: snap to preset with 0.08 lerp factor
Zustand ringStore updates trigger a buildRing() call that rebuilds the Three.js scene. Avoid full scene recreation — update material colors and geometry in-place where possible for performance.
POST /chat # Body: {messages: ChatMessage[], config?: RingConfig}
# Returns: SSE stream of Claude response
POST /generate # Body: {prompt: string, config: RingConfig}
# Returns: {glb_url: string, generation_id: string}
POST /recognize # Body: multipart/form-data image
# Returns: {detected_components: string[], suggested_config: RingConfig}
GET /export/stl/{id} # Returns: STL file download
GET /health # Returns: {status: "ok"}
- Use functional components only, no class components
- Named exports for all components, default export only for pages
- Props interfaces named
ComponentNameProps - All async operations wrapped in
try/catchwith typed error states - No
anytypes — useunknownand narrow - Tailwind only — no inline styles, no CSS modules
- Pydantic v2 for all request/response models
- Async route handlers (
async def) throughout - All external API calls in
services/— never directly in routers - Environment variables via
python-dotenv, validated on startup - Return
HTTPExceptionwith clear status codes, never raw exceptions
- React components:
PascalCase.tsx - Utilities / hooks:
camelCase.ts - Python modules:
snake_case.py
Format: type(scope): message
Types: feat fix refactor style docs chore
Example: feat(chat): add streaming Claude response parser
- Node.js 18+ installed
- Python 3.11+ installed
-
frontend/.env.localcreated with all required keys -
backend/.envcreated with all required keys - Anthropic API key obtained from console.anthropic.com
- OpenAI API key obtained from platform.openai.com
- HuggingFace token obtained from huggingface.co/settings/tokens
- Vercel project linked to frontend/
- Render service linked to backend/
- Rings only — no necklaces, bracelets, earrings yet
- Static procedural geometry in MVP — TripoSG generation is an async job (show skeleton + progress bar)
- No user accounts — configs are session-only in MVP
- Free-tier Render — backend cold starts ~30s; show loading state
- No AR — future feature post-MVP
- Ring sizing (inner diameter, US/EU/IN sizing)
- Engraving text preview
- AR try-on via WebXR / MediaPipe hand tracking
- STL export for 3D printing
- User accounts + saved designs
- Image upload → component recognition (GPT-4o Vision full pipeline)
- Price estimation based on metal weight + gem carat
- Expand to necklaces, earrings, bracelets