Architecture — Shader versioning & draft system:
- New shader_versions table: immutable snapshots of every edit
- Shaders now have status: draft, published, archived
- current_version counter tracks version number
- Every create/update creates a ShaderVersion record
- Restore-from-version endpoint creates new version (never destructive)
- Drafts are private, only visible to author
- Forks start as drafts
- Free tier rate limit applies only to published shaders (drafts unlimited)
Architecture — Platform identity:
- System account 'fractafrag' (UUID 00000000-...-000001) created in init.sql
- is_system flag on users and shaders
- system_label field: 'fractafrag-curated', future: 'fractafrag-generated'
- Feed/explore can filter by is_system
- System shaders display distinctly from user/AI content
API changes:
- GET /shaders/mine — user workspace (drafts, published, archived)
- GET /shaders/{id}/versions — version history
- GET /shaders/{id}/versions/{n} — specific version
- POST /shaders/{id}/versions/{n}/restore — restore old version
- POST /shaders accepts status: 'draft' | 'published'
- PUT /shaders/{id} accepts change_note for version descriptions
- PUT status transitions: draft→published, published→archived, archived→published
Frontend — Editor improvements:
- Resizable split pane with drag handle (20-80% range, smooth col-resize cursor)
- Save Draft button (creates/updates as draft, no publish)
- Publish button (validates, publishes, redirects to shader page)
- Version badge shows current version number when editing existing
- Owner detection: editing own shader vs forking someone else's
- Saved status indicator ('Draft saved', 'Published')
Frontend — My Shaders workspace:
- /my-shaders route with status tabs (All, Draft, Published, Archived)
- Count badges per tab
- Status badges on shader cards (draft=yellow, published=green, archived=grey)
- Version badges (v1, v2, etc.)
- Quick actions: Edit, Publish, Archive, Restore, Delete per status
- Drafts link to editor, published link to detail page
Seed data — 200 fractafrag-curated shaders:
- 171 2D + 29 3D shaders
- 500 unique tags across all shaders
- All 200 titles are unique
- Covers: fractals (Mandelbrot, Julia sets), noise (fbm, Voronoi, Perlin),
raymarching (metaballs, terrain, torus knots, metall/glass),
effects (glitch, VHS, plasma, aurora, lightning, fireworks),
patterns (circuit, hex grid, stained glass, herringbone, moiré),
physics (wave interference, pendulum, caustics, gravity lens),
minimal (single shapes, gradients, dot grids),
nature (ink, watercolor, smoke, sand garden, coral, nebula),
color theory (RGB separation, CMY overlap, hue wheel),
domain warping (acid trip, lava rift, storm eye),
particles (fireflies, snow, ember, bubbles)
- Each shader has style_metadata (chaos_level, color_temperature, motion_type)
- Distributed creation times over 30 days for feed ranking variety
- Random initial scores for algorithm testing
- All authored by 'fractafrag' system account, is_system=true
- system_label='fractafrag-curated' for clear provenance
Schema:
- shader_versions table with (shader_id, version_number) unique constraint
- HNSW indexes on version lookup
- System account indexes
- Status-aware feed indexes
240 lines
8 KiB
Python
240 lines
8 KiB
Python
"""Fractafrag — Pydantic Request/Response Schemas."""
|
|
|
|
from __future__ import annotations
|
|
from datetime import datetime
|
|
from uuid import UUID
|
|
from typing import Optional
|
|
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# AUTH
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class UserRegister(BaseModel):
|
|
username: str = Field(..., min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_-]+$")
|
|
email: EmailStr
|
|
password: str = Field(..., min_length=8, max_length=128)
|
|
turnstile_token: str
|
|
|
|
|
|
class UserLogin(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
turnstile_token: str
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
|
|
|
|
class UserPublic(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
username: str
|
|
role: str
|
|
is_system: bool
|
|
subscription_tier: str
|
|
is_verified_creator: bool
|
|
created_at: datetime
|
|
|
|
|
|
class UserMe(UserPublic):
|
|
email: str
|
|
ai_credits_remaining: int
|
|
trust_tier: str
|
|
last_active_at: Optional[datetime] = None
|
|
|
|
|
|
class UserUpdate(BaseModel):
|
|
username: Optional[str] = Field(None, min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_-]+$")
|
|
email: Optional[EmailStr] = None
|
|
|
|
|
|
class ByokKeysUpdate(BaseModel):
|
|
anthropic_key: Optional[str] = Field(None, description="Anthropic API key")
|
|
openai_key: Optional[str] = Field(None, description="OpenAI API key")
|
|
ollama_endpoint: Optional[str] = Field(None, description="Ollama endpoint URL")
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# SHADERS
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class ShaderCreate(BaseModel):
|
|
title: str = Field(..., min_length=1, max_length=120)
|
|
description: Optional[str] = Field(None, max_length=1000)
|
|
glsl_code: str = Field(..., min_length=10)
|
|
tags: list[str] = Field(default_factory=list, max_length=10)
|
|
shader_type: str = Field(default="2d", pattern=r"^(2d|3d|audio-reactive)$")
|
|
is_public: bool = True
|
|
status: str = Field(default="published", pattern=r"^(draft|published)$")
|
|
style_metadata: Optional[dict] = None
|
|
fulfills_desire_id: Optional[UUID] = None
|
|
|
|
|
|
class ShaderUpdate(BaseModel):
|
|
title: Optional[str] = Field(None, min_length=1, max_length=120)
|
|
description: Optional[str] = Field(None, max_length=1000)
|
|
glsl_code: Optional[str] = Field(None, min_length=10)
|
|
tags: Optional[list[str]] = None
|
|
is_public: Optional[bool] = None
|
|
status: Optional[str] = Field(None, pattern=r"^(draft|published|archived)$")
|
|
change_note: Optional[str] = Field(None, max_length=200)
|
|
|
|
|
|
class ShaderPublic(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
author_id: Optional[UUID]
|
|
title: str
|
|
description: Optional[str]
|
|
glsl_code: str
|
|
status: str
|
|
is_public: bool
|
|
is_ai_generated: bool
|
|
is_system: bool
|
|
ai_provider: Optional[str]
|
|
system_label: Optional[str]
|
|
thumbnail_url: Optional[str]
|
|
preview_url: Optional[str]
|
|
render_status: str
|
|
style_metadata: Optional[dict]
|
|
tags: list[str]
|
|
shader_type: str
|
|
forked_from: Optional[UUID]
|
|
current_version: int
|
|
view_count: int
|
|
score: float
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class ShaderFeedItem(BaseModel):
|
|
"""Lighter shader representation for feed responses."""
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
author_id: Optional[UUID]
|
|
title: str
|
|
thumbnail_url: Optional[str]
|
|
preview_url: Optional[str]
|
|
glsl_code: str
|
|
tags: list[str]
|
|
shader_type: str
|
|
score: float
|
|
view_count: int
|
|
is_ai_generated: bool
|
|
is_system: bool
|
|
system_label: Optional[str]
|
|
style_metadata: Optional[dict]
|
|
created_at: datetime
|
|
|
|
|
|
class ShaderVersionPublic(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
shader_id: UUID
|
|
version_number: int
|
|
glsl_code: str
|
|
title: str
|
|
description: Optional[str]
|
|
tags: list[str]
|
|
style_metadata: Optional[dict]
|
|
change_note: Optional[str]
|
|
thumbnail_url: Optional[str]
|
|
created_at: datetime
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# VOTES & ENGAGEMENT
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class VoteCreate(BaseModel):
|
|
value: int = Field(..., ge=-1, le=1)
|
|
|
|
|
|
class DwellReport(BaseModel):
|
|
shader_id: UUID
|
|
dwell_secs: float = Field(..., gt=0)
|
|
replayed: bool = False
|
|
session_id: Optional[str] = None
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# DESIRES / BOUNTIES
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class DesireCreate(BaseModel):
|
|
prompt_text: str = Field(..., min_length=5, max_length=500)
|
|
style_hints: Optional[dict] = None
|
|
|
|
|
|
class DesirePublic(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
author_id: Optional[UUID]
|
|
prompt_text: str
|
|
style_hints: Optional[dict]
|
|
tip_amount_cents: int
|
|
status: str
|
|
heat_score: float
|
|
fulfilled_by_shader: Optional[UUID]
|
|
fulfilled_at: Optional[datetime]
|
|
created_at: datetime
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# AI GENERATION
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class GenerateRequest(BaseModel):
|
|
prompt: str = Field(..., min_length=5, max_length=500)
|
|
provider: Optional[str] = None
|
|
style_metadata: Optional[dict] = None
|
|
|
|
|
|
class GenerateStatusResponse(BaseModel):
|
|
job_id: str
|
|
status: str
|
|
shader_id: Optional[UUID] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# API KEYS
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class ApiKeyCreate(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=100)
|
|
|
|
|
|
class ApiKeyPublic(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
key_prefix: str
|
|
name: Optional[str]
|
|
trust_tier: str
|
|
rate_limit_per_hour: int
|
|
last_used_at: Optional[datetime]
|
|
created_at: datetime
|
|
|
|
|
|
class ApiKeyCreated(ApiKeyPublic):
|
|
full_key: str
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════
|
|
# PAGINATION
|
|
# ════════════════════════════════════════════════════════════
|
|
|
|
class PaginatedResponse(BaseModel):
|
|
items: list
|
|
cursor: Optional[str] = None
|
|
has_more: bool = False
|