# artdag-common Shared components for Art-DAG L1 (celery) and L2 (activity-pub) servers. ## Features - **Jinja2 Templating**: Unified template environment with shared base templates - **Reusable Components**: Cards, tables, pagination, DAG visualization, media preview - **Authentication Middleware**: Cookie and JWT token parsing - **Content Negotiation**: HTML/JSON/ActivityPub format detection - **Utility Functions**: Hash truncation, file size formatting, status colors ## Installation ```bash pip install -e /path/to/artdag-common # Or add to requirements.txt -e file:../common ``` ## Quick Start ```python from fastapi import FastAPI, Request from artdag_common import create_jinja_env, render app = FastAPI() # Initialize templates with app-specific directory templates = create_jinja_env("app/templates") @app.get("/") async def home(request: Request): return render(templates, "home.html", request, title="Home") ``` ## Package Structure ``` artdag_common/ ├── __init__.py # Package exports ├── constants.py # CDN URLs, colors, configs ├── rendering.py # Jinja2 environment and helpers ├── middleware/ │ ├── auth.py # Authentication utilities │ └── content_negotiation.py # Accept header parsing ├── models/ │ ├── requests.py # Shared request models │ └── responses.py # Shared response models ├── utils/ │ ├── formatting.py # Text/date formatting │ ├── media.py # Media type detection │ └── pagination.py # Pagination helpers └── templates/ ├── base.html # Base layout template └── components/ ├── badge.html # Status/type badges ├── card.html # Info cards ├── dag.html # Cytoscape DAG visualization ├── media_preview.html # Video/image/audio preview ├── pagination.html # HTMX pagination └── table.html # Styled tables ``` ## Jinja2 Templates ### Base Template The `base.html` template provides: - Dark theme with Tailwind CSS - HTMX integration - Navigation slot - Content block - Optional Cytoscape.js block ```html {% extends "base.html" %} {% block title %}My Page{% endblock %} {% block content %}

Hello World

{% endblock %} ``` ### Reusable Components #### Card ```html {% include "components/card.html" %} ``` ```html
{% block card_title %}Title{% endblock %} {% block card_content %}Content{% endblock %}
``` #### Badge Status and type badges with appropriate colors: ```html {% from "components/badge.html" import status_badge, type_badge %} {{ status_badge("completed") }} {{ status_badge("failed") }} {{ type_badge("video") }} ``` #### DAG Visualization Interactive Cytoscape.js graph: ```html {% include "components/dag.html" %} ``` Requires passing `nodes` and `edges` data to template context. #### Media Preview Responsive media preview with format detection: ```html {% include "components/media_preview.html" %} ``` Supports video, audio, and image formats. #### Pagination HTMX-powered infinite scroll pagination: ```html {% include "components/pagination.html" %} ``` ## Template Rendering ### Full Page Render ```python from artdag_common import render @app.get("/runs/{run_id}") async def run_detail(run_id: str, request: Request): run = get_run(run_id) return render(templates, "runs/detail.html", request, run=run) ``` ### Fragment Render (HTMX) ```python from artdag_common import render_fragment @app.get("/runs/{run_id}/status") async def run_status_fragment(run_id: str): run = get_run(run_id) html = render_fragment(templates, "components/status.html", status=run.status) return HTMLResponse(html) ``` ## Authentication Middleware ### UserContext ```python from artdag_common.middleware.auth import UserContext, get_user_from_cookie @app.get("/profile") async def profile(request: Request): user = get_user_from_cookie(request) if not user: return RedirectResponse("/login") return {"username": user.username, "actor_id": user.actor_id} ``` ### Token Parsing ```python from artdag_common.middleware.auth import get_user_from_header, decode_jwt_claims @app.get("/api/me") async def api_me(request: Request): user = get_user_from_header(request) if not user: raise HTTPException(401, "Not authenticated") return {"user": user.username} ``` ## Content Negotiation Detect what response format the client wants: ```python from artdag_common.middleware.content_negotiation import wants_html, wants_json, wants_activity_json @app.get("/users/{username}") async def user_profile(username: str, request: Request): user = get_user(username) if wants_activity_json(request): return ActivityPubActor(user) elif wants_json(request): return user.dict() else: return render(templates, "users/profile.html", request, user=user) ``` ## Constants ### CDN URLs ```python from artdag_common import TAILWIND_CDN, HTMX_CDN, CYTOSCAPE_CDN # Available in templates as globals: # {{ TAILWIND_CDN }} # {{ HTMX_CDN }} # {{ CYTOSCAPE_CDN }} ``` ### Node Colors ```python from artdag_common import NODE_COLORS # { # "SOURCE": "#3b82f6", # Blue # "EFFECT": "#22c55e", # Green # "OUTPUT": "#a855f7", # Purple # "ANALYSIS": "#f59e0b", # Amber # "_LIST": "#6366f1", # Indigo # "default": "#6b7280", # Gray # } ``` ### Status Colors ```python STATUS_COLORS = { "completed": "bg-green-600", "cached": "bg-blue-600", "running": "bg-yellow-600", "pending": "bg-gray-600", "failed": "bg-red-600", } ``` ## Custom Jinja2 Filters The following filters are available in all templates: | Filter | Usage | Description | |--------|-------|-------------| | `truncate_hash` | `{{ hash\|truncate_hash }}` | Shorten hash to 16 chars with ellipsis | | `format_size` | `{{ bytes\|format_size }}` | Format bytes as KB/MB/GB | | `status_color` | `{{ status\|status_color }}` | Get Tailwind class for status | Example: ```html {{ run.status }} {{ content_hash|truncate_hash }} {{ file_size|format_size }} ``` ## Development ```bash cd /root/art-dag/common # Install in development mode pip install -e . # Run tests pytest ``` ## Dependencies - `fastapi>=0.100.0` - Web framework - `jinja2>=3.1.0` - Templating engine - `pydantic>=2.0.0` - Data validation