A free training tool for concept artists to practice breaking down client briefs into actionable design questions.
Live site: https://concept-art-training.vercel.app/
- AI-Generated Client Briefs - Realistic briefs with configurable difficulty, genre, design type, and more
- Question Breakdown Workspace - Organize your analysis into categories (silhouette, color, materials, etc.)
- Client Q&A Simulator - Chat with simulated clients who have different personalities
- Research Suggestions - AI-generated research topics based on the brief
- Reveal Mode - Compare your breakdown with an AI-generated ideal after submission
- Progress Dashboard - Track which design types and difficulties you've practiced
- Brief Sharing - Share briefs with other artists via URL
- Terminology Glossary - Learn industry terms as you practice
This app requires users to provide their own AI API key. Supported providers:
- Anthropic (Claude)
- OpenAI (GPT-4, GPT-3.5)
- Google AI (Gemini)
- OpenRouter (Multiple models)
- Groq (Fast & cheap Llama, Mixtral)
API keys are stored locally in the browser and never sent to any server except the user's chosen AI provider.
# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview- Push this code to a GitHub repository
- Go to vercel.com and sign in with GitHub
- Click "New Project" and import your repository
- Click "Deploy" - that's it!
Vercel will automatically detect it's a Vite project and configure everything.
- Push this code to a GitHub repository
- Go to netlify.com and sign in with GitHub
- Click "New site from Git" and select your repository
- Build settings (should auto-detect):
- Build command:
npm run build - Publish directory:
dist
- Build command:
- Click "Deploy site"
-
Install gh-pages:
npm install -D gh-pages -
Add to package.json scripts:
"predeploy": "npm run build", "deploy": "gh-pages -d dist"
-
Add to vite.config.js:
base: '/your-repo-name/',
-
Run:
npm run deploy
- Push to GitHub
- Go to pages.cloudflare.com
- Connect your repository
- Build settings:
- Framework preset: Vite
- Build command:
npm run build - Build output:
dist
# Build the image
docker build -t concept-art-trainer .
# Run locally
docker run -p 8080:80 concept-art-trainer
# Or use docker-compose
docker-compose upThen open http://localhost:8080
- Push your image to Amazon ECR:
# Login to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
# Create repository
aws ecr create-repository --repository-name concept-art-trainer
# Tag and push
docker tag concept-art-trainer:latest YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/concept-art-trainer:latest
docker push YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/concept-art-trainer:latest- Go to AWS App Runner Console
- Click "Create service"
- Select "Container registry" → "Amazon ECR"
- Select your image
- Configure:
- Port: 80
- CPU: 0.25 vCPU (plenty for static site)
- Memory: 0.5 GB
- Deploy
Cost: ~$5-15/month for minimal traffic
- Push image to ECR (same as above)
- Create an ECS Cluster (Fargate)
- Create a Task Definition:
{
"family": "concept-art-trainer",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "concept-art-trainer",
"image": "YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/concept-art-trainer:latest",
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp"
}
],
"healthCheck": {
"command": [
"CMD-SHELL",
"wget --quiet --tries=1 --spider http://localhost:80/health || exit 1"
],
"interval": 30,
"timeout": 5,
"retries": 3
}
}
]
}- Create a Service with an Application Load Balancer
- Go to Lightsail Console
- Click "Containers" → "Create container service"
- Choose the $7/month nano plan
- Deploy from local image
- Configure public endpoint on port 80
# Install Lightsail plugin
aws lightsailctl
# Push image
aws lightsail push-container-image \
--service-name concept-art-trainer \
--label latest \
--image concept-art-trainer:latest- Push image to Azure Container Registry:
# Login to Azure
az login
# Create resource group
az group create --name concept-art-rg --location eastus
# Create container registry
az acr create --resource-group concept-art-rg --name conceptarttrainer --sku Basic
# Login to ACR
az acr login --name conceptarttrainer
# Tag and push
docker tag concept-art-trainer conceptarttrainer.azurecr.io/concept-art-trainer:latest
docker push conceptarttrainer.azurecr.io/concept-art-trainer:latest- Deploy to Container Apps:
# Create Container Apps environment
az containerapp env create \
--name concept-art-env \
--resource-group concept-art-rg \
--location eastus
# Deploy the app
az containerapp create \
--name concept-art-trainer \
--resource-group concept-art-rg \
--environment concept-art-env \
--image conceptarttrainer.azurecr.io/concept-art-trainer:latest \
--target-port 80 \
--ingress external \
--registry-server conceptarttrainer.azurecr.io \
--cpu 0.25 \
--memory 0.5Gi \
--min-replicas 0 \
--max-replicas 1Cost: Can scale to zero, pay only for usage (~$0-10/month for low traffic)
- Push to ACR (same as above)
- Create App Service:
# Create App Service plan
az appservice plan create \
--name concept-art-plan \
--resource-group concept-art-rg \
--is-linux \
--sku B1
# Create web app
az webapp create \
--resource-group concept-art-rg \
--plan concept-art-plan \
--name concept-art-trainer \
--deployment-container-image-name conceptarttrainer.azurecr.io/concept-art-trainer:latest
# Configure ACR access
az webapp config container set \
--name concept-art-trainer \
--resource-group concept-art-rg \
--docker-registry-server-url https://conceptarttrainer.azurecr.io# One command deployment
az container create \
--resource-group concept-art-rg \
--name concept-art-trainer \
--image conceptarttrainer.azurecr.io/concept-art-trainer:latest \
--dns-name-label concept-art-trainer \
--ports 80 \
--cpu 0.5 \
--memory 0.5Access at: http://concept-art-trainer.eastus.azurecontainer.io
# Build and push to Google Container Registry
gcloud builds submit --tag gcr.io/YOUR_PROJECT/concept-art-trainer
# Deploy
gcloud run deploy concept-art-trainer \
--image gcr.io/YOUR_PROJECT/concept-art-trainer \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--port 80 \
--cpu 1 \
--memory 256Mi \
--min-instances 0 \
--max-instances 1Cost: Free tier includes 2 million requests/month
- Push to Docker Hub or DigitalOcean Container Registry
- Create App → Select "Docker Hub" or "DOCR"
- Configure:
- HTTP Port: 80
- Instance Size: Basic ($5/month)
- Deploy
Create .github/workflows/deploy.yml:
name: Build and Deploy
on:
push:
branches: [main]
env:
IMAGE_NAME: concept-art-trainer
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t $IMAGE_NAME .
- name: Log in to registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin ${{ secrets.REGISTRY_URL }}
- name: Push image
run: |
docker tag $IMAGE_NAME ${{ secrets.REGISTRY_URL }}/$IMAGE_NAME:latest
docker tag $IMAGE_NAME ${{ secrets.REGISTRY_URL }}/$IMAGE_NAME:${{ github.sha }}
docker push ${{ secrets.REGISTRY_URL }}/$IMAGE_NAME:latest
docker push ${{ secrets.REGISTRY_URL }}/$IMAGE_NAME:${{ github.sha }}Add your registry secrets in GitHub → Settings → Secrets.
This project uses Vitest with React Testing Library for testing.
| Metric | Coverage |
|---|---|
| Statements | 99.62% |
| Branches | 93.43% |
| Functions | 100% |
| Lines | 99.62% |
# Run tests in watch mode
npm test
# Run tests once
npm run test:run
# Run tests with UI
npm run test:ui
# Run tests with coverage
npm run test:coverageTests are co-located with their source files:
Component.tsx→Component.test.tsxmodule.ts→module.test.ts
// Example component test
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('should call onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});Edit the API_PROVIDERS object in src/App.tsx:
const API_PROVIDERS = {
// ... existing providers
newprovider: {
name: 'New Provider',
endpoint: 'https://api.newprovider.com/v1/chat',
modelsEndpoint: 'https://api.newprovider.com/v1/models',
keyPlaceholder: 'np-...',
docsUrl: 'https://newprovider.com/api-keys',
},
};Then add the corresponding fetch logic in fetchModels() and API call logic in callLLM().
Edit the DESIGN_TYPES array and add corresponding entries in CONDITIONAL_OPTIONS.
Edit the GLOSSARY object to add new terms and definitions.
MIT License - feel free to use, modify, and distribute.
Built with:
Inspired by concept art training exercises from the game and film industry.