yonigozlan's picture
yonigozlan HF Staff
Fix wrong model title
d7256dc
#!/usr/bin/env python3
"""
Beautiful custom timeline visualization for Transformers models using Flask.
"""
import glob
import os
import re
import sys
import time
import webbrowser
from datetime import datetime
from typing import Dict, List, Optional
from flask import Flask, jsonify, render_template, request
import transformers
class TransformersTimelineParser:
"""Parser for extracting model release dates from Transformers documentation."""
def __init__(self, docs_dir: str):
self.docs_dir = docs_dir
self.models_cache = None
self.tasks_cache = {}
# Add transformers source directory to Python path to import auto mappings
transformers_src = os.path.join(os.path.dirname(docs_dir), "..", "..", "src")
if transformers_src not in sys.path:
sys.path.insert(0, transformers_src)
# Modality definitions with modern color scheme
self.modalities = {
"text": {
"name": "Text Models",
"color": "#F59E0B", # Soft amber
"models": [
"albert",
"apertus",
"arcee",
"bamba",
"bart",
"barthez",
"bartpho",
"bert",
"bert-generation",
"bert-japanese",
"bertweet",
"big_bird",
"bigbird_pegasus",
"biogpt",
"bitnet",
"blenderbot",
"blenderbot-small",
"bloom",
"bort",
"byt5",
"camembert",
"canine",
"codegen",
"code_llama",
"cohere",
"cohere2",
"convbert",
"cpm",
"cpmant",
"ctrl",
"dbrx",
"deberta",
"deberta-v2",
"deepseek_v3",
"dialogpt",
"diffllama",
"distilbert",
"doge",
"dots1",
"dpr",
"electra",
"encoder-decoder",
"ernie",
"ernie4_5",
"ernie4_5_moe",
"ernie_m",
"esm",
"exaone4",
"falcon",
"falcon3",
"falcon_h1",
"falcon_mamba",
"flan-t5",
"flan-ul2",
"flaubert",
"fnet",
"fsmt",
"funnel",
"fuyu",
"gemma",
"gemma2",
"glm",
"glm4",
"glm4_moe",
"openai-gpt",
"gpt_neo",
"gpt_neox",
"gpt_neox_japanese",
"gptj",
"gpt2",
"gpt_bigcode",
"gpt_oss",
"gptsan-japanese",
"gpt-sw3",
"granite",
"granitemoe",
"granitemoehybrid",
"granitemoeshared",
"helium",
"herbert",
"hgnet_v2",
"hunyuan_v1_dense",
"hunyuan_v1_moe",
"ibert",
"jamba",
"jetmoe",
"jukebox",
"led",
"lfm2",
"llama",
"llama2",
"llama3",
"longformer",
"longt5",
"luke",
"m2m_100",
"madlad-400",
"mamba",
"mamba2",
"marian",
"markuplm",
"mbart",
"mega",
"megatron-bert",
"megatron_gpt2",
"minimax",
"ministral",
"mistral",
"mixtral",
"mluke",
"mobilebert",
"modernbert",
"modernbert-decoder",
"mpnet",
"mpt",
"mra",
"mt5",
"mvp",
"myt5",
"nemotron",
"nezha",
"nllb",
"nllb-moe",
"nystromformer",
"olmo",
"olmo2",
"olmo3",
"olmoe",
"open-llama",
"opt",
"pegasus",
"pegasus_x",
"persimmon",
"phi",
"phi3",
"phimoe",
"phobert",
"plbart",
"prophetnet",
"qdqbert",
"qwen2",
"qwen2_moe",
"qwen3",
"qwen3_moe",
"qwen3_next",
"rag",
"realm",
"recurrent_gemma",
"reformer",
"rembert",
"retribert",
"roberta",
"roberta-prelayernorm",
"roc_bert",
"roformer",
"rwkv",
"seed_oss",
"splinter",
"squeezebert",
"stablelm",
"starcoder2",
"switch_transformers",
"t5",
"t5gemma",
"t5v1.1",
"tapex",
"transfo-xl",
"ul2",
"umt5",
"vaultgemma",
"xmod",
"xglm",
"xlm",
"xlm-prophetnet",
"xlm-roberta",
"xlm-roberta-xl",
"xlm-v",
"xlnet",
"xlstm",
"yoso",
"zamba",
"zamba2",
],
},
"vision": {
"name": "Vision Models",
"color": "#06B6D4", # Soft cyan
"models": [
"aimv2",
"beit",
"bit",
"conditional_detr",
"convnext",
"convnextv2",
"cvt",
"d_fine",
"dab-detr",
"deepseek_v2",
"deepseek_vl",
"deepseek_vl_hybrid",
"deformable_detr",
"deit",
"depth_anything",
"depth_anything_v2",
"depth_pro",
"deta",
"detr",
"dinat",
"dinov2",
"dinov2_with_registers",
"dinov3",
"dit",
"dpt",
"efficientformer",
"efficientloftr",
"efficientnet",
"eomt",
"focalnet",
"glpn",
"hgnet_v2",
"hiera",
"ijepa",
"imagegpt",
"levit",
"lightglue",
"mask2former",
"maskformer",
"mlcd",
"mobilenet_v1",
"mobilenet_v2",
"mobilevit",
"mobilevitv2",
"nat",
"poolformer",
"prompt_depth_anything",
"pvt",
"pvt_v2",
"regnet",
"resnet",
"rt_detr",
"rt_detr_v2",
"segformer",
"seggpt",
"superglue",
"superpoint",
"swiftformer",
"swin",
"swinv2",
"swin2sr",
"table-transformer",
"textnet",
"timm_wrapper",
"upernet",
"van",
"vit",
"vit_hybrid",
"vitdet",
"vit_mae",
"vitmatte",
"vit_msn",
"vitpose",
"yolos",
"zoedepth",
],
},
"audio": {
"name": "Audio Models",
"color": "#8B5CF6", # Soft purple
"models": [
"audio-spectrogram-transformer",
"bark",
"clap",
"csm",
"dac",
"dia",
"encodec",
"fastspeech2_conformer",
"granite_speech",
"hubert",
"kyutai_speech_to_text",
"mctct",
"mimi",
"mms",
"moonshine",
"moshi",
"musicgen",
"musicgen_melody",
"pop2piano",
"seamless_m4t",
"seamless_m4t_v2",
"sew",
"sew-d",
"speech_to_text",
"speech_to_text_2",
"speecht5",
"unispeech",
"unispeech-sat",
"univnet",
"vits",
"wav2vec2",
"wav2vec2-bert",
"wav2vec2-conformer",
"wav2vec2_phoneme",
"wavlm",
"whisper",
"xcodec",
"xls_r",
"xlsr_wav2vec2",
],
},
"video": {
"name": "Video Models",
"color": "#EC4899", # Soft pink
"models": ["timesformer", "vjepa2", "videomae", "vivit"],
},
"multimodal": {
"name": "Multimodal Models",
"color": "#10B981", # Soft emerald
"models": [
"align",
"altclip",
"aria",
"aya_vision",
"blip",
"blip-2",
"bridgetower",
"bros",
"chameleon",
"chinese_clip",
"clip",
"clipseg",
"clvp",
"cohere2_vision",
"colpali",
"colqwen2",
"data2vec",
"deplot",
"donut",
"emu3",
"evolla",
"flava",
"florence2",
"gemma3",
"gemma3n",
"git",
"glm4v",
"glm4v_moe",
"got_ocr2",
"granitevision",
"grounding-dino",
"groupvit",
"idefics",
"idefics2",
"idefics3",
"instructblip",
"instructblipvideo",
"internvl",
"janus",
"kosmos-2",
"kosmos2_5",
"layoutlm",
"layoutlmv2",
"layoutlmv3",
"layoutxlm",
"lilt",
"llama4",
"llava",
"llava_next",
"llava_next_video",
"llava_onevision",
"lxmert",
"matcha",
"metaclip_2",
"mgp-str",
"mistral3",
"mllama",
"mm-grounding-dino",
"nougat",
"omdet-turbo",
"oneformer",
"ovis2",
"owlvit",
"owlv2",
"paligemma",
"perceiver",
"perception_lm",
"phi4_multimodal",
"pix2struct",
"pixtral",
"qwen2_5_omni",
"qwen2_5_vl",
"qwen2_audio",
"qwen2_vl",
"qwen3_vl",
"qwen3_vl_moe",
"sam2",
"sam2_video",
"sam",
"sam_hq",
"shieldgemma2",
"siglip",
"siglip2",
"smollm3",
"smolvlm",
"speech-encoder-decoder",
"tapas",
"trocr",
"tvlt",
"tvp",
"udop",
"video_llava",
"vilt",
"vipllava",
"vision-encoder-decoder",
"vision-text-dual-encoder",
"visual_bert",
"voxtral",
"xclip",
],
},
"reinforcement": {
"name": "Reinforcement Learning",
"color": "#EF4444", # Soft red
"models": ["decision_transformer", "trajectory_transformer"],
},
"timeseries": {
"name": "Time Series Models",
"color": "#F97316", # Soft orange
"models": ["autoformer", "informer", "patchtsmixer", "patchtst", "time_series_transformer", "timesfm"],
},
"graph": {
"name": "Graph Models",
"color": "#6B7280", # Soft gray
"models": ["graphormer"],
},
}
def get_model_modality(self, model_name: str) -> Dict[str, str]:
"""Determine the modality category for a given model."""
for modality_key, modality_info in self.modalities.items():
if model_name in modality_info["models"]:
return {"key": modality_key, "name": modality_info["name"], "color": modality_info["color"]}
# Default to text if not found (most common)
return {"key": "text", "name": "Text Models", "color": "#F59E0B"}
def parse_release_date_from_file(self, file_path: str) -> Optional[Dict[str, str]]:
"""Parse the release date line from a model documentation file."""
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Extract model name from file path (always available)
model_name = os.path.basename(file_path).replace(".md", "")
# Initialize default values
release_date = None
transformers_date = None
# Focus on the end of the sentence - the Transformers addition date is what matters most
pattern = (
r"\*This model was released on (.+?) and added to Hugging Face Transformers on (\d{4}-\d{2}-\d{2})\.\*"
)
match = re.search(pattern, content)
if match:
release_date = match.group(1).strip()
transformers_date = match.group(2)
# Validate the Transformers date (this is the critical one for our timeline)
try:
datetime.strptime(transformers_date, "%Y-%m-%d")
except ValueError:
return None
# Handle release_date - could be "None" or an actual date
if release_date.lower() == "none":
release_date = None
else:
# Try to validate as a date, but don't fail if it's not
try:
datetime.strptime(release_date, "%Y-%m-%d")
except ValueError:
# Keep the original value even if it's not a valid date
pass
else:
# No release date pattern found - warn and skip (ignore auto.md intentionally)
base = os.path.basename(file_path)
if base != "auto.md":
print(f"⚠️ Warning: No release/addition dates found in {file_path}; skipping.")
return None
# Get modality information
modality = self.get_model_modality(model_name)
# Extract model description
description = self.extract_model_description(content)
# Get supported tasks/pipelines
tasks = self.get_model_tasks(model_name)
return {
"model_name": model_name,
"file_path": file_path,
"release_date": release_date,
"transformers_date": transformers_date,
"modality": modality["key"],
"modality_name": modality["name"],
"modality_color": modality["color"],
"description": description,
"tasks": tasks,
}
except Exception as e:
print(f"Error processing {file_path}: {e}")
return None
def extract_model_description(self, content: str) -> str:
"""Extract the first 1000 characters of model description, excluding HTML/XML tags."""
try:
# Remove HTML/XML tags
content_no_tags = re.sub(r"<[^>]+>", "", content)
# Find the start of the actual description (after the initial metadata)
# Look for the first substantial paragraph after the initial lines
lines = content_no_tags.split("\n")
description_start = 0
# Skip initial metadata, imports, and short lines
for i, line in enumerate(lines):
stripped = line.strip()
if (
len(stripped) > 50
and not stripped.startswith("#")
and not stripped.startswith("*This model was released")
and not stripped.startswith("<!--")
and not stripped.startswith("from ")
and not stripped.startswith("import ")
and "preview" not in stripped.lower()
and not stripped.startswith(">>>")
):
description_start = i
break
# Join the description lines and limit to 1000 characters
description_lines = lines[description_start:]
description = "\n".join(description_lines).strip()
if len(description) > 1000:
description = description[:1000]
# Try to cut at a word boundary
last_space = description.rfind(" ")
if last_space > 800: # Only cut at word boundary if it's not too short
description = description[:last_space]
description += "..."
return description
except Exception as e:
print(f"Error extracting description: {e}")
return "No description available."
def load_model_task_mappings(self) -> Dict[str, List[str]]:
"""Load model-to-task mappings from transformers auto model mappings."""
if self.tasks_cache:
return self.tasks_cache
try:
# Import the model mappings from transformers
from transformers.models.auto.modeling_auto import (
MODEL_FOR_AUDIO_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_CAUSAL_LM_MAPPING_NAMES,
MODEL_FOR_DEPTH_ESTIMATION_MAPPING_NAMES,
MODEL_FOR_DOCUMENT_QUESTION_ANSWERING_MAPPING_NAMES,
MODEL_FOR_IMAGE_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_IMAGE_SEGMENTATION_MAPPING_NAMES,
MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES,
MODEL_FOR_IMAGE_TO_IMAGE_MAPPING_NAMES,
MODEL_FOR_INSTANCE_SEGMENTATION_MAPPING_NAMES,
MODEL_FOR_MASK_GENERATION_MAPPING_NAMES,
MODEL_FOR_MASKED_LM_MAPPING_NAMES,
MODEL_FOR_OBJECT_DETECTION_MAPPING_NAMES,
MODEL_FOR_QUESTION_ANSWERING_MAPPING_NAMES,
MODEL_FOR_SEMANTIC_SEGMENTATION_MAPPING_NAMES,
MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING_NAMES,
MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_TABLE_QUESTION_ANSWERING_MAPPING_NAMES,
MODEL_FOR_TEXT_TO_SPECTROGRAM_MAPPING_NAMES,
MODEL_FOR_TIME_SERIES_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_TIME_SERIES_PREDICTION_MAPPING_NAMES,
MODEL_FOR_TIME_SERIES_REGRESSION_MAPPING_NAMES,
MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_UNIVERSAL_SEGMENTATION_MAPPING_NAMES,
MODEL_FOR_VIDEO_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES,
MODEL_FOR_VISUAL_QUESTION_ANSWERING_MAPPING_NAMES,
MODEL_FOR_ZERO_SHOT_IMAGE_CLASSIFICATION_MAPPING_NAMES,
MODEL_FOR_ZERO_SHOT_OBJECT_DETECTION_MAPPING_NAMES,
)
# Define the mapping from MODEL_FOR_* to human-readable task names
task_mappings = {
"text-generation": MODEL_FOR_CAUSAL_LM_MAPPING_NAMES,
"text-classification": MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING_NAMES,
"token-classification": MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING_NAMES,
"question-answering": MODEL_FOR_QUESTION_ANSWERING_MAPPING_NAMES,
"fill-mask": MODEL_FOR_MASKED_LM_MAPPING_NAMES,
"text2text-generation": MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING_NAMES,
"image-classification": MODEL_FOR_IMAGE_CLASSIFICATION_MAPPING_NAMES,
"object-detection": MODEL_FOR_OBJECT_DETECTION_MAPPING_NAMES,
"image-segmentation": MODEL_FOR_IMAGE_SEGMENTATION_MAPPING_NAMES,
"semantic-segmentation": MODEL_FOR_SEMANTIC_SEGMENTATION_MAPPING_NAMES,
"instance-segmentation": MODEL_FOR_INSTANCE_SEGMENTATION_MAPPING_NAMES,
"universal-segmentation": MODEL_FOR_UNIVERSAL_SEGMENTATION_MAPPING_NAMES,
"depth-estimation": MODEL_FOR_DEPTH_ESTIMATION_MAPPING_NAMES,
"video-classification": MODEL_FOR_VIDEO_CLASSIFICATION_MAPPING_NAMES,
"audio-classification": MODEL_FOR_AUDIO_CLASSIFICATION_MAPPING_NAMES,
"image-to-text": MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES,
"image-text-to-text": MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES,
"visual-question-answering": MODEL_FOR_VISUAL_QUESTION_ANSWERING_MAPPING_NAMES,
"document-question-answering": MODEL_FOR_DOCUMENT_QUESTION_ANSWERING_MAPPING_NAMES,
"table-question-answering": MODEL_FOR_TABLE_QUESTION_ANSWERING_MAPPING_NAMES,
"zero-shot-image-classification": MODEL_FOR_ZERO_SHOT_IMAGE_CLASSIFICATION_MAPPING_NAMES,
"zero-shot-object-detection": MODEL_FOR_ZERO_SHOT_OBJECT_DETECTION_MAPPING_NAMES,
"image-to-image": MODEL_FOR_IMAGE_TO_IMAGE_MAPPING_NAMES,
"mask-generation": MODEL_FOR_MASK_GENERATION_MAPPING_NAMES,
"text-to-audio": MODEL_FOR_TEXT_TO_SPECTROGRAM_MAPPING_NAMES,
"time-series-classification": MODEL_FOR_TIME_SERIES_CLASSIFICATION_MAPPING_NAMES,
"time-series-regression": MODEL_FOR_TIME_SERIES_REGRESSION_MAPPING_NAMES,
"time-series-prediction": MODEL_FOR_TIME_SERIES_PREDICTION_MAPPING_NAMES,
}
# Invert the mapping: model_name -> [list of tasks]
model_to_tasks = {}
for task_name, model_mapping in task_mappings.items():
for model_name in model_mapping.keys():
if model_name not in model_to_tasks:
model_to_tasks[model_name] = []
model_to_tasks[model_name].append(task_name)
self.tasks_cache = model_to_tasks
print(f"✅ Loaded task mappings for {len(model_to_tasks)} models")
return model_to_tasks
except Exception as e:
print(f"❌ Error loading task mappings: {e}")
return {}
def get_model_tasks(self, model_name: str) -> List[str]:
"""Get the list of tasks/pipelines supported by a model."""
if not self.tasks_cache:
self.load_model_task_mappings()
# Normalize model name (handle variations)
normalized_name = model_name.lower().replace("_", "-")
# Try exact match first
if normalized_name in self.tasks_cache:
return self.tasks_cache[normalized_name]
# Try some common variations
variations = [
model_name.lower(),
model_name.replace("_", "-"),
model_name.replace("-", "_"),
]
for variation in variations:
if variation in self.tasks_cache:
return self.tasks_cache[variation]
return []
def extract_model_title_from_file(self, file_path: str) -> str:
"""Extract the model title from the markdown file."""
try:
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
line = line.strip()
if line.startswith("```python"):
break
if line.startswith("# ") and not line.startswith("# Overview"):
title = line[2:].strip()
return title
except Exception:
pass
return os.path.basename(file_path).replace(".md", "").replace("_", " ").replace("-", " ").title()
def parse_all_model_dates(self, force_refresh: bool = False) -> List[Dict[str, str]]:
"""Parse release dates from all model documentation files."""
if self.models_cache is not None and not force_refresh:
return self.models_cache
models = []
pattern = os.path.join(self.docs_dir, "*.md")
md_files = glob.glob(pattern)
print(f"Found {len(md_files)} markdown files to process...")
for file_path in md_files:
result = self.parse_release_date_from_file(file_path)
if result:
result["display_name"] = self.extract_model_title_from_file(file_path)
models.append(result)
models.sort(key=lambda x: x["transformers_date"])
print(f"Found {len(models)} models with release dates")
self.models_cache = models
return models
# Initialize Flask app
app = Flask(__name__)
# Dynamically find the transformers installation directory
transformers_path = os.path.dirname(transformers.__file__)
# Check if transformers repo exists, if not clone it
repo_dir = os.path.join(os.path.dirname(__file__), "transformers")
if not os.path.exists(repo_dir):
print("Cloning transformers repository...")
import subprocess
subprocess.run(["git", "clone", "https://github.com/huggingface/transformers.git", repo_dir], check=True)
docs_dir = os.path.join(repo_dir, "docs", "source", "en", "model_doc")
docs_dir = os.path.abspath(docs_dir)
parser = TransformersTimelineParser(docs_dir)
@app.route("/")
def index():
"""Main timeline page."""
return render_template("timeline.html")
@app.route("/api/models")
def get_models():
"""API endpoint to get all models with date, modality, and task filtering."""
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
modalities = request.args.getlist("modality") # Get list of selected modalities
tasks = request.args.getlist("task") # Get list of selected tasks
try:
models = parser.parse_all_model_dates()
# Apply modality filtering
if modalities:
models = [model for model in models if model["modality"] in modalities]
# Apply task filtering
if tasks:
# Filter models that support at least one of the selected tasks
models = [model for model in models if any(task in model.get("tasks", []) for task in tasks)]
# Apply date filtering
if start_date and end_date:
filtered_models = [m for m in models if start_date <= m["transformers_date"] <= end_date]
else:
filtered_models = models
return jsonify(
{
"success": True,
"models": filtered_models,
"total_count": len(models),
"filtered_count": len(filtered_models),
}
)
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/modalities")
def get_modalities():
"""API endpoint to get available modalities."""
try:
modalities = []
for key, info in parser.modalities.items():
modalities.append({"key": key, "name": info["name"], "color": info["color"]})
return jsonify({"success": True, "modalities": modalities})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/tasks")
def get_tasks():
"""API endpoint to get available tasks/pipelines."""
try:
# Load task mappings to get all available tasks
parser.load_model_task_mappings()
# Collect all unique tasks across all models
all_tasks = set()
for model_tasks in parser.tasks_cache.values():
all_tasks.update(model_tasks)
# Define task categories and colors
task_categories = {
"text-generation": {"name": "Text Generation", "color": "#6366f1"},
"text-classification": {"name": "Text Classification", "color": "#8b5cf6"},
"token-classification": {"name": "Token Classification", "color": "#a855f7"},
"question-answering": {"name": "Question Answering", "color": "#c084fc"},
"fill-mask": {"name": "Fill Mask", "color": "#d8b4fe"},
"text2text-generation": {"name": "Text2Text Generation", "color": "#e879f9"},
"image-classification": {"name": "Image Classification", "color": "#06b6d4"},
"object-detection": {"name": "Object Detection", "color": "#0891b2"},
"image-segmentation": {"name": "Image Segmentation", "color": "#0e7490"},
"semantic-segmentation": {"name": "Semantic Segmentation", "color": "#155e75"},
"instance-segmentation": {"name": "Instance Segmentation", "color": "#164e63"},
"universal-segmentation": {"name": "Universal Segmentation", "color": "#1e40af"},
"depth-estimation": {"name": "Depth Estimation", "color": "#1d4ed8"},
"zero-shot-image-classification": {"name": "Zero-Shot Image Classification", "color": "#2563eb"},
"zero-shot-object-detection": {"name": "Zero-Shot Object Detection", "color": "#3b82f6"},
"image-to-image": {"name": "Image to Image", "color": "#60a5fa"},
"mask-generation": {"name": "Mask Generation", "color": "#93c5fd"},
"image-to-text": {"name": "Image to Text", "color": "#10b981"},
"image-text-to-text": {"name": "Image+Text to Text", "color": "#059669"},
"visual-question-answering": {"name": "Visual Question Answering", "color": "#047857"},
"document-question-answering": {"name": "Document Question Answering", "color": "#065f46"},
"table-question-answering": {"name": "Table Question Answering", "color": "#064e3b"},
"video-classification": {"name": "Video Classification", "color": "#dc2626"},
"audio-classification": {"name": "Audio Classification", "color": "#ea580c"},
"text-to-audio": {"name": "Text to Audio", "color": "#f97316"},
"time-series-classification": {"name": "Time Series Classification", "color": "#84cc16"},
"time-series-regression": {"name": "Time Series Regression", "color": "#65a30d"},
"time-series-prediction": {"name": "Time Series Prediction", "color": "#4d7c0f"},
}
# Return tasks that actually exist in the mappings
available_tasks = []
for task in sorted(all_tasks):
if task in task_categories:
available_tasks.append(
{"key": task, "name": task_categories[task]["name"], "color": task_categories[task]["color"]}
)
else:
# Fallback for unmapped tasks
available_tasks.append(
{
"key": task,
"name": task.replace("-", " ").title(),
"color": "#6b7280", # Gray color for unmapped tasks
}
)
return jsonify({"success": True, "tasks": available_tasks})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
def create_timeline_template():
"""Create the HTML template for the timeline."""
template_dir = os.path.join(os.path.dirname(__file__), "templates")
os.makedirs(template_dir, exist_ok=True)
html_content = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤗 Transformers Models Timeline</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 100%);
min-height: 100vh;
color: #333;
display: flex;
flex-direction: column;
}
.header {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
padding: 0.8rem 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
position: sticky;
top: 0;
z-index: 100;
margin: 0 0 0.3rem 0;
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 0.1rem;
}
.header p {
color: #666;
font-size: 0.8rem;
margin: 0;
}
.controls-wrapper {
margin: 0.3rem 2rem;
}
.controls-toggle {
position: absolute;
left: 8px;
right: 8px;
top: 8px;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
font-size: 0.9rem;
border-radius: 999px;
border: 1px solid #e5e7eb;
background: #ffffff;
color: #374151;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.controls-toggle:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0,0,0,0.1);
}
.controls {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
padding: 0.4rem 1rem;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
display: flex;
gap: 0.8rem;
align-items: start;
flex-wrap: nowrap;
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.2s ease;
position: relative;
padding-left: 2.4rem; /* space for the left toggle button */
max-height: none; /* will be set dynamically */
}
.controls.collapsed {
opacity: 1;
}
/* Blur fade to hint there is more content when collapsed */
.controls.collapsed::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 26px;
background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0.94) 60%, rgba(255,255,255,1));
pointer-events: none;
}
.input-group {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 120px;
}
.input-group label {
font-weight: 500;
color: #4a5568;
font-size: 0.7rem;
}
.input-group input {
padding: 0.3rem 0.6rem;
border: 1px solid #e2e8f0;
border-radius: 5px;
font-size: 0.7rem;
transition: all 0.2s ease;
background: white;
}
.input-group input:focus {
outline: none;
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.1);
}
.modality-group {
flex: 22%;
min-width: 220px;
padding-left: 8px; /* add small spacing from collapse arrow */
}
.modality-filters {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.2rem;
}
.modality-buttons {
display: flex;
gap: 0.3rem;
margin-top: 0.3rem;
}
.task-group {
flex: 78%;
min-width: 520px;
}
.task-filters {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.2rem;
max-height: 140px;
overflow-y: auto;
}
.task-buttons {
display: flex;
gap: 0.3rem;
margin-top: 0.3rem;
}
.btn-small {
padding: 0.2rem 0.4rem;
font-size: 0.65rem;
margin-top: 0;
}
.modality-checkbox {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.5rem;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 0.65rem;
font-weight: 500;
position: relative;
overflow: hidden;
}
.modality-checkbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: currentColor;
opacity: 0;
transition: opacity 0.3s ease;
}
.modality-checkbox:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
border-color: currentColor;
}
.modality-checkbox:hover::before {
opacity: 0.05;
}
.modality-checkbox.checked {
background: rgba(255, 255, 255, 0.95);
border-color: currentColor;
}
.modality-checkbox.checked::before {
opacity: 0.08;
}
.modality-checkbox input[type="checkbox"] {
appearance: none;
width: 16px;
height: 16px;
border: 2px solid var(--modality-color, #8B5CF6);
border-radius: 4px;
position: relative;
margin: 0;
background: white;
transition: all 0.2s ease;
flex-shrink: 0;
}
.modality-checkbox input[type="checkbox"]:checked {
background: var(--modality-color, #8B5CF6);
border-color: var(--modality-color, #8B5CF6);
}
.modality-checkbox input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 10px;
font-weight: bold;
line-height: 1;
}
.modality-checkbox label {
cursor: pointer;
user-select: none;
color: #374151;
font-weight: 600;
}
/* Task filter styles (mirroring modality styles) */
.task-checkbox {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.5rem;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 0.65rem;
font-weight: 500;
position: relative;
overflow: hidden;
}
.task-checkbox::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: currentColor;
opacity: 0;
transition: opacity 0.3s ease;
}
.task-checkbox:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
border-color: currentColor;
}
.task-checkbox:hover::before {
opacity: 0.05;
}
.task-checkbox.checked {
background: rgba(255, 255, 255, 0.95);
border-color: currentColor;
}
.task-checkbox.checked::before {
opacity: 0.08;
}
.task-checkbox input[type="checkbox"] {
appearance: none;
width: 16px;
height: 16px;
border: 2px solid var(--task-color, #6366f1);
border-radius: 4px;
background: white;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
}
.task-checkbox input[type="checkbox"]:checked {
background: var(--task-color, #6366f1);
border-color: var(--task-color, #6366f1);
}
.task-checkbox input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 10px;
font-weight: bold;
line-height: 1;
}
.task-checkbox label {
cursor: pointer;
user-select: none;
color: #374151;
font-weight: 600;
}
.modality-checkbox input[type="checkbox"]:not(:checked) {
background: white;
border-color: var(--modality-color, #8B5CF6);
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1.1rem;
align-self: flex-start;
}
.btn-primary {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(217, 119, 6, 0.3);
}
.btn-secondary {
background: #f7fafc;
color: #4a5568;
border: 2px solid #e2e8f0;
}
.timeline-container {
margin: 0.5rem 2rem 1rem 2rem;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
position: relative;
flex: 1;
display: flex;
flex-direction: column;
height: calc(100vh - 110px);
}
.timeline-wrapper {
position: relative;
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
cursor: grab;
user-select: none;
}
.timeline-wrapper:active {
cursor: grabbing;
}
.timeline-scroll {
position: relative;
width: 100%;
height: 100%;
cursor: inherit;
user-select: none;
}
.timeline-scroll::-webkit-scrollbar {
display: none;
}
.timeline {
position: absolute;
width: 100%;
height: 100%;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.2rem;
color: #d97706;
border: 2px solid rgba(217, 119, 6, 0.2);
backdrop-filter: blur(10px);
transition: all 0.2s ease;
z-index: 10;
}
.nav-arrow:hover {
background: rgba(217, 119, 6, 0.1);
border-color: #d97706;
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 16px rgba(217, 119, 6, 0.3);
}
.nav-arrow.left {
left: 10px;
}
.nav-arrow.right {
right: 10px;
}
.nav-arrow:disabled {
opacity: 0.3;
cursor: not-allowed;
transform: translateY(-50%) scale(0.9);
}
.zoom-controls {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 8px;
z-index: 20;
}
.zoom-btn {
width: 36px;
height: 36px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
color: #d97706;
border: 2px solid rgba(217, 119, 6, 0.2);
backdrop-filter: blur(10px);
transition: all 0.2s ease;
user-select: none;
}
.zoom-btn:hover {
background: rgba(217, 119, 6, 0.1);
border-color: #d97706;
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(217, 119, 6, 0.3);
}
.zoom-btn:active {
transform: scale(0.95);
}
.zoom-indicator {
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
padding: 4px 8px;
font-size: 0.75rem;
color: #d97706;
font-weight: 500;
border: 1px solid rgba(217, 119, 6, 0.2);
}
.timeline-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #fbbf24, #d97706);
border-radius: 2px;
transform: translateY(-50%);
}
.timeline-item {
position: absolute;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
transition: all 0.3s ease;
}
.timeline-item:hover {
transform: translateY(-50%);
z-index: 10;
}
.timeline-connector {
position: absolute;
width: 1px;
background: #d2d6dc;
left: 50%;
transform: translateX(-50%);
opacity: 0.6;
z-index: 1;
}
.date-marker {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: #9ca3af;
opacity: 0.8;
pointer-events: none;
z-index: 1; /* In background, below everything */
}
.date-label {
position: absolute;
top: 20px; /* Near the top of timeline viewport */
left: 8px; /* Offset to the right of the line */
font-size: 0.75rem;
color: #6b7280;
font-weight: 500;
background: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 6px;
pointer-events: none;
border: 1px solid rgba(156, 163, 175, 0.4);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
white-space: nowrap;
z-index: 2; /* Just above the dotted line but below everything else */
backdrop-filter: blur(4px);
}
.date-marker.year {
opacity: 0.9;
background: #6b7280;
width: 3px;
}
.date-marker.year .date-label {
font-weight: 600;
color: #4b5563;
background: rgba(255, 255, 255, 0.9);
font-size: 0.8rem;
border-color: #9ca3af;
}
.date-marker.quarter {
opacity: 0.7;
}
.date-marker.month {
opacity: 0.8;
}
.timeline-dot {
width: 18px;
height: 18px;
border-radius: 50%;
background: white;
border: 3px solid;
position: relative;
margin: 0 auto;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.2);
z-index: 5;
}
.timeline-item:nth-child(odd) .timeline-dot {
border-color: #fbbf24;
}
.timeline-item:nth-child(even) .timeline-dot {
border-color: #d97706;
}
.timeline-label {
position: absolute;
min-width: 100px;
max-width: 160px;
padding: 0.4rem 0.6rem;
background: white;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
text-align: center;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12);
border: 1px solid #e2e8f0;
line-height: 1.2;
word-break: break-word;
left: 50%;
transform: translateX(-50%);
z-index: 6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.timeline-label:not(.expanded):hover {
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.15),
0 0 4px var(--modality-color, #8B5CF6);
border-color: var(--modality-color, #8B5CF6);
}
/* Expanded card styles */
.timeline-label.expanded {
max-width: 500px !important;
min-width: 400px !important;
width: 500px !important;
min-height: 250px !important;
text-align: left;
z-index: 9999 !important; /* Much higher than all other elements */
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
user-select: text;
}
/* Cards above axis - expand downward with TOP edge fixed */
.timeline-item.above-1 .timeline-label.expanded,
.timeline-item.above-2 .timeline-label.expanded,
.timeline-item.above-3 .timeline-label.expanded {
/* JavaScript will set the exact top position to keep top edge fixed */
transform: translateX(-50%);
}
/* Cards below axis - expand upward with BOTTOM edge fixed */
.timeline-item.below-1 .timeline-label.expanded,
.timeline-item.below-2 .timeline-label.expanded,
.timeline-item.below-3 .timeline-label.expanded {
/* JavaScript will set the exact bottom position to keep bottom edge fixed */
transform: translateX(-50%);
}
.timeline-label .model-title {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: #1f2937;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.timeline-label .model-description {
font-size: 0.7rem;
line-height: 1.4;
color: #4b5563;
margin-bottom: 0.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.timeline-label.expanded .model-description {
max-height: 200px !important;
height: 200px !important;
position: relative;
overflow: hidden; /* Hide overflow for blur effect */
}
.timeline-label .description-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.timeline-label.expanded .description-content {
max-height: 200px !important;
height: 200px !important;
overflow-y: auto;
padding-right: 8px; /* Space for scrollbar */
}
.timeline-label .description-fade {
position: absolute;
bottom: -3px; /* Move down 3px to cover text artifact */
left: 0;
right: 0;
height: 43px; /* Increase height to compensate */
background: linear-gradient(to bottom,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 50%,
rgba(255, 255, 255, 1) 100%);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 10;
}
.timeline-label.expanded .description-fade {
opacity: 1;
}
/* Markdown-style formatting */
.timeline-label .model-description h1,
.timeline-label .model-description h2,
.timeline-label .model-description h3 {
font-weight: 600;
margin: 0.8em 0 0.4em 0;
color: #1f2937;
}
.timeline-label .model-description h1 { font-size: 0.85rem; }
.timeline-label .model-description h2 { font-size: 0.8rem; }
.timeline-label .model-description h3 { font-size: 0.75rem; }
.timeline-label .model-description p {
margin: 0.5em 0;
}
.timeline-label .model-description code {
background: #f3f4f6;
padding: 0.1em 0.3em;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.65rem;
}
.timeline-label .model-description strong {
font-weight: 600;
color: #1f2937;
}
.timeline-label .model-description em {
font-style: italic;
}
.timeline-label .model-description ul,
.timeline-label .model-description ol {
margin: 0.5em 0;
padding-left: 1.2em;
}
.timeline-label .model-description li {
margin: 0.2em 0;
}
.timeline-label .model-description a {
color: var(--modality-color, #8B5CF6);
text-decoration: underline;
font-weight: 500;
}
.timeline-label .model-description a:hover {
color: #1f2937;
text-decoration: none;
}
.timeline-label .learn-more {
display: none;
text-decoration: none;
color: var(--modality-color, #8B5CF6);
font-size: 0.7rem;
font-weight: 600;
padding: 0.3rem 0.6rem;
border: 1px solid var(--modality-color, #8B5CF6);
border-radius: 4px;
background: rgba(255, 255, 255, 0.8);
transition: all 0.2s ease;
margin-top: 0.5rem;
text-align: center;
}
.timeline-label.expanded .learn-more {
display: inline-block;
}
.timeline-label .learn-more:hover {
background: var(--modality-color, #8B5CF6);
color: white;
transform: translateY(-1px);
}
.timeline-label.expanded .learn-more {
pointer-events: auto;
cursor: pointer;
}
.timeline-label.expanded .model-description a {
pointer-events: auto;
cursor: pointer;
}
.timeline-label.expanded .description-content {
pointer-events: auto;
cursor: text;
}
.timeline-label.expanded .model-tasks {
pointer-events: none;
cursor: default;
}
.model-tasks {
margin: 0.5rem 0;
padding: 0.4rem 0.6rem;
background: rgba(248, 250, 252, 0.8);
border-radius: 8px;
border: 1px solid rgba(226, 232, 240, 0.5);
}
.tasks-label {
font-size: 0.7rem;
font-weight: 600;
color: #4a5568;
margin-bottom: 0.3rem;
}
.tasks-list {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.task-badge {
display: inline-block;
padding: 0.2rem 0.4rem;
color: white;
border-radius: 4px;
font-size: 0.6rem;
font-weight: 500;
opacity: 0.9;
transition: all 0.2s ease;
}
.task-badge:hover {
opacity: 1;
transform: scale(1.05);
}
/* Above the axis - wave pattern */
.timeline-item.above-1 .timeline-label {
bottom: 60px;
border-left: 3px solid #fbbf24;
}
.timeline-item.above-1 .timeline-connector {
bottom: 9px;
height: 60px;
}
.timeline-item.above-2 .timeline-label {
bottom: 120px;
border-left: 3px solid #fbbf24;
}
.timeline-item.above-2 .timeline-connector {
bottom: 9px;
height: 120px;
}
.timeline-item.above-3 .timeline-label {
bottom: 180px;
border-left: 3px solid #fbbf24;
}
.timeline-item.above-3 .timeline-connector {
bottom: 9px;
height: 180px;
}
/* Below the axis - wave pattern */
.timeline-item.below-1 .timeline-label {
top: 60px;
border-left: 3px solid #d97706;
}
.timeline-item.below-1 .timeline-connector {
top: 9px;
height: 60px;
}
.timeline-item.below-2 .timeline-label {
top: 120px;
border-left: 3px solid #d97706;
}
.timeline-item.below-2 .timeline-connector {
top: 9px;
height: 120px;
}
.timeline-item.below-3 .timeline-label {
top: 180px;
border-left: 3px solid #d97706;
}
.timeline-item.below-3 .timeline-connector {
top: 9px;
height: 180px;
}
.timeline-date {
font-size: 0.65rem;
color: #9ca3af;
margin-top: 0.3rem;
font-weight: 500;
}
.date-controls {
display: flex;
gap: 0.8rem;
margin-left: auto;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 0.6rem 1rem;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.date-input-group {
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
}
.date-input-group label {
font-size: 0.7rem;
color: #4a5568;
font-weight: 600;
white-space: nowrap;
}
.date-input-group input {
padding: 0.4rem 0.6rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 0.75rem;
background: white;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 130px;
text-align: center;
font-weight: 500;
color: #374151;
}
.date-input-group input:hover {
border-color: #cbd5e0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.date-input-group input:focus {
outline: none;
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.15);
transform: translateY(-1px);
}
.stats {
display: flex;
gap: 0.6rem;
margin: 0.3rem 2rem 2rem 2rem; /* Added bottom margin for page */
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.stat-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
padding: 0.4rem 0.8rem;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
flex: 1;
min-width: 140px;
text-align: center;
}
.stat-number {
font-size: 1.2rem;
font-weight: 600;
color: #d97706;
display: block;
}
.stat-label {
color: #666;
font-size: 0.7rem;
margin-top: 0.15rem;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 1.1rem;
color: #666;
}
.loading::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid #e2e8f0;
border-top: 2px solid #d97706;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
text-align: center;
padding: 2rem;
color: #e53e3e;
background: #fed7d7;
border-radius: 8px;
margin: 2rem;
}
</style>
</head>
<body>
<div class="header">
<h1>🤗 Transformers Models Timeline</h1>
<p>Interactive timeline to explore models supported by the Hugging Face Transformers library!</p>
</div>
<div class="controls-wrapper">
<div class="controls collapsed" id="filtersPanel">
<button id="toggleFilters" class="controls-toggle" type="button" aria-label="Toggle filters" onclick="toggleFilters()">▸</button>
<div class="input-group modality-group">
<label>Modalities</label>
<div class="modality-filters" id="modalityFilters">
<!-- Modality checkboxes will be populated by JavaScript -->
</div>
<div class="modality-buttons">
<button class="btn btn-primary btn-small" onclick="checkAllModalities()">
Check All
</button>
<button class="btn btn-secondary btn-small" onclick="clearAllModalities()">
Clear All
</button>
</div>
</div>
<div class="input-group task-group">
<label>Tasks/Pipelines</label>
<div class="task-filters" id="taskFilters">
<!-- Task checkboxes will be populated by JavaScript -->
</div>
<div class="task-buttons">
<button class="btn btn-primary btn-small" onclick="checkAllTasks()">
Check All
</button>
<button class="btn btn-secondary btn-small" onclick="clearAllTasks()">
Clear All
</button>
</div>
</div>
</div>
</div>
<div class="timeline-container">
<div class="timeline-wrapper">
<div class="nav-arrow left" id="navLeft">‹</div>
<div class="nav-arrow right" id="navRight">›</div>
<div class="zoom-controls">
<div class="zoom-btn" id="zoomOut" title="Zoom out (show more models)">−</div>
<div class="zoom-indicator" id="zoomLevel">100%</div>
<div class="zoom-btn" id="zoomIn" title="Zoom in (spread models apart)">+</div>
</div>
<div class="timeline-scroll" id="timelineScroll">
<div class="timeline" id="timeline">
<div class="loading">Loading timeline...</div>
</div>
</div>
</div>
</div>
<div class="stats" id="stats">
<div class="stat-card">
<span class="stat-number" id="totalCount">-</span>
<div class="stat-label">Total Models</div>
</div>
<div class="stat-card">
<span class="stat-number" id="displayedCount">-</span>
<div class="stat-label">Displayed Models</div>
</div>
<div class="stat-card">
<span class="stat-number" id="dateRange">-</span>
<div class="stat-label">Date Range</div>
</div>
<div class="date-controls">
<div class="date-input-group">
<label for="startDate">Start Date</label>
<input type="date" id="startDate" />
</div>
<div class="date-input-group">
<label for="endDate">End Date</label>
<input type="date" id="endDate" />
</div>
</div>
</div>
<script>
let allModels = [];
let currentModels = [];
let timelineOffset = 0;
let timelineWidth = 0;
let containerWidth = 0;
let dateInitialized = false; // initialize date inputs once from data
let isDragging = false;
let startX = 0;
let startOffset = 0;
let zoomLevel = 1.0; // 1.0 = 100%, 0.5 = 50%, 2.0 = 200%
const minZoom = 0.3; // Minimum zoom (30%)
const maxZoom = 3.0; // Maximum zoom (300%)
async function loadTimeline() {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '<div class="loading">Loading timeline...</div>';
try {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
// Get selected modalities
const selectedModalities = Array.from(document.querySelectorAll('.modality-checkbox input:checked'))
.map(checkbox => checkbox.value);
// Get selected tasks
const selectedTasks = Array.from(document.querySelectorAll('.task-checkbox input:checked'))
.map(checkbox => checkbox.value);
let url = '/api/models';
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
selectedModalities.forEach(modality => params.append('modality', modality));
selectedTasks.forEach(task => params.append('task', task));
if (params.toString()) url += '?' + params.toString();
const response = await fetch(url);
const data = await response.json();
if (!data.success) {
throw new Error(data.error);
}
allModels = data.models;
currentModels = data.models;
// Initialize date inputs to min/max once (based on current data)
if (!dateInitialized && currentModels && currentModels.length > 0) {
const validDates = currentModels
.map(m => m.transformers_date)
.filter(d => !!d)
.sort();
if (validDates.length > 0) {
const minDate = validDates[0];
const maxDate = validDates[validDates.length - 1];
const startEl = document.getElementById('startDate');
const endEl = document.getElementById('endDate');
if (startEl) {
startEl.min = minDate;
startEl.max = maxDate;
if (!startEl.value) startEl.value = minDate;
}
if (endEl) {
endEl.min = minDate;
endEl.max = maxDate;
if (!endEl.value) endEl.value = maxDate;
}
dateInitialized = true;
}
}
updateStats(data.total_count, data.filtered_count);
renderTimeline(currentModels);
} catch (error) {
timeline.innerHTML = `<div class="error">Error loading timeline: ${error.message}</div>`;
}
}
function updateStats(totalCount, displayedCount) {
document.getElementById('totalCount').textContent = totalCount;
document.getElementById('displayedCount').textContent = displayedCount;
if (currentModels.length > 0) {
const firstDate = currentModels[0].transformers_date;
const lastDate = currentModels[currentModels.length - 1].transformers_date;
document.getElementById('dateRange').textContent = `${firstDate} — ${lastDate}`;
} else {
document.getElementById('dateRange').textContent = 'No data';
}
}
function formatDate(dateString) {
if (!dateString || dateString === 'Unknown Date') return 'Unknown Date';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (error) {
return dateString; // Return original if parsing fails
}
}
function createFaintColor(hexColor) {
// Remove # if present
hexColor = hexColor.replace('#', '');
// Parse RGB values
const r = parseInt(hexColor.substr(0, 2), 16);
const g = parseInt(hexColor.substr(2, 2), 16);
const b = parseInt(hexColor.substr(4, 2), 16);
// Reduce saturation by mixing with a lighter gray (keeping brightness)
const lightGray = 200; // Light gray to maintain brightness
const desaturatedR = Math.floor(r * 0.6 + lightGray * 0.4);
const desaturatedG = Math.floor(g * 0.6 + lightGray * 0.4);
const desaturatedB = Math.floor(b * 0.6 + lightGray * 0.4);
// Add some opacity (70%)
return `rgba(${desaturatedR}, ${desaturatedG}, ${desaturatedB}, 0.7)`;
}
function markdownToHtml(markdown) {
if (!markdown) return '';
// Simple markdown to HTML conversion
let html = markdown
// Links first (before other processing)
.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank" onclick="event.stopPropagation()">$1</a>')
// Headers (handle # at start of line)
.replace(/^#{3}\\s+(.*$)/gm, '<h3>$1</h3>')
.replace(/^#{2}\\s+(.*$)/gm, '<h2>$1</h2>')
.replace(/^#{1}\\s+(.*$)/gm, '<h1>$1</h1>')
// Bold and italic
.replace(/\\*\\*\\*([^*]+)\\*\\*\\*/g, '<strong><em>$1</em></strong>')
.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')
.replace(/\\*([^*]+)\\*/g, '<em>$1</em>')
// Code (inline)
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Lists (simple)
.replace(/^-\\s+(.+$)/gm, '<li>$1</li>')
// Split into paragraphs and process
.split('\\n\\n')
.map(paragraph => {
paragraph = paragraph.trim();
if (!paragraph) return '';
// Don't wrap headers, lists, or already wrapped content in paragraphs
if (paragraph.match(/^<h[1-6]>|^<li>|^<ul>|^<ol>|^<p>/)) {
return paragraph;
}
// Wrap other content in paragraphs
return '<p>' + paragraph.replace(/\\n/g, '<br>') + '</p>';
})
.filter(p => p.length > 0)
.join('')
// Wrap consecutive list items in ul tags
.replace(/(<li>.*?<\\/li>)/gs, '<ul>$1</ul>')
// Clean up multiple consecutive ul tags
.replace(/<\\/ul>\\s*<ul>/g, '');
return html;
}
function renderTimeline(models, preservePosition = false) {
const timeline = document.getElementById('timeline');
const timelineScroll = document.getElementById('timelineScroll');
if (models.length === 0) {
timeline.innerHTML = '<div class="loading">No models found with the current filters</div>';
return;
}
// Sort models chronologically by transformers_date for proper timeline display
const sortedModels = [...models].sort((a, b) =>
new Date(a.transformers_date) - new Date(b.transformers_date)
);
// Calculate dimensions with zoom level
containerWidth = timelineScroll.clientWidth;
const baseSpacing = 80; // Base spacing between models
const actualSpacing = baseSpacing * zoomLevel;
timelineWidth = Math.max(containerWidth, sortedModels.length * actualSpacing + 200);
timeline.style.width = timelineWidth + 'px';
// Clear timeline and add centered line
timeline.innerHTML = '<div class="timeline-line"></div>';
// Add date markers before adding models
addDateMarkers(sortedModels, actualSpacing);
// Create wave patterns for stacking - simpler pattern
const abovePattern = [1, 2, 3]; // 3 levels above
const belowPattern = [1, 2, 3]; // 3 levels below
let aboveIndex = 0;
let belowIndex = 0;
// Add model items with wave positioning
sortedModels.forEach((model, index) => {
const position = (index * actualSpacing) + 100; // Linear spacing with zoom
const item = document.createElement('div');
// Alternate between above and below the axis
let positionClass;
if (index % 2 === 0) {
// Above the axis
positionClass = `above-${abovePattern[aboveIndex % abovePattern.length]}`;
aboveIndex++;
} else {
// Below the axis
positionClass = `below-${belowPattern[belowIndex % belowPattern.length]}`;
belowIndex++;
}
item.className = `timeline-item ${positionClass}`;
item.style.left = position + 'px';
const dot = document.createElement('div');
dot.className = 'timeline-dot';
// Use modality color for background and a darker version for border
const modalityColor = model.modality_color || '#8B5CF6';
const darkenColor = (hex, percent = 30) => {
const num = parseInt(hex.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = Math.max(0, (num >> 16) - amt);
const G = Math.max(0, (num >> 8 & 0x00FF) - amt);
const B = Math.max(0, (num & 0x0000FF) - amt);
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
};
dot.style.backgroundColor = modalityColor;
dot.style.borderColor = darkenColor(modalityColor, 10);
const connector = document.createElement('div');
connector.className = 'timeline-connector';
const label = document.createElement('div');
label.className = 'timeline-label';
label.style.borderLeftColor = model.modality_color || '#8B5CF6';
// Set the modality color as a CSS custom property for hover effects
label.style.setProperty('--modality-color', model.modality_color || '#8B5CF6');
// Ensure the model name is always displayed
const modelName = model.display_name || model.model_name || 'Unknown Model';
const modelDate = formatDate(model.transformers_date || 'Unknown Date');
const description = model.description || 'No description available.';
// Set initial compact content (no tasks, description, or learn more)
label.innerHTML = `
<div class="model-title">${modelName}</div>
<div class="timeline-date">${modelDate}</div>
`;
// Store expanded content data for later use
label.dataset.modelName = modelName;
label.dataset.modelDate = modelDate;
label.dataset.description = description;
label.dataset.learnMoreUrl = `https://huggingface.co/docs/transformers/main/en/model_doc/${model.model_name}`;
if (model.tasks) {
label.dataset.tasks = JSON.stringify(model.tasks);
}
// Add click handler for expansion
label.addEventListener('click', (e) => {
e.stopPropagation();
// Don't toggle if clicking inside an expanded card
if (label.classList.contains('expanded')) {
return;
}
// Close any other expanded cards
document.querySelectorAll('.timeline-label.expanded').forEach(otherLabel => {
if (otherLabel !== label) {
otherLabel.classList.remove('expanded');
// Restore compact content for other cards
setTimeout(() => {
otherLabel.innerHTML = `
<div class="model-title">${otherLabel.dataset.modelName}</div>
<div class="timeline-date">${otherLabel.dataset.modelDate}</div>
`;
// Reset positioning after content change
setTimeout(() => {
otherLabel.style.top = '';
otherLabel.style.bottom = '';
otherLabel.parentElement.style.zIndex = '';
}, 50);
}, 50);
}
});
// Check if currently expanded
const isExpanding = !label.classList.contains('expanded');
if (isExpanding) {
// Calculate current position BEFORE changing content
const rect = label.getBoundingClientRect();
const containerRect = label.parentElement.getBoundingClientRect();
const currentTop = rect.top - containerRect.top;
const currentBottom = containerRect.bottom - rect.bottom;
// Determine if this is above or below axis
const isAboveAxis = positionClass.includes('above');
// Generate expanded content
const storedTasks = label.dataset.tasks ? JSON.parse(label.dataset.tasks) : [];
const formattedDescription = markdownToHtml(label.dataset.description);
// Format tasks for display with task-specific colors
const tasksHtml = storedTasks && storedTasks.length > 0 ?
`<div class="model-tasks">
<div class="tasks-label">Supported Tasks:</div>
<div class="tasks-list">
${storedTasks.map(task => {
const taskColor = getTaskColor(task);
return `<span class="task-badge" style="background-color: ${taskColor}">${task.replace(/-/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase())}</span>`;
}).join('')}
</div>
</div>` :
'<div class="model-tasks"><div class="tasks-label">No tasks available</div></div>';
// Set expanded content
label.innerHTML = `
<div class="model-title">${label.dataset.modelName}</div>
<div class="timeline-date">${label.dataset.modelDate}</div>
${tasksHtml}
<div class="model-description">
<div class="description-content">${formattedDescription}</div>
<div class="description-fade"></div>
</div>
<a href="${label.dataset.learnMoreUrl}"
target="_blank"
class="learn-more"
onclick="event.stopPropagation()">
Learn More →
</a>
`;
// Add expanded class
label.classList.add('expanded');
// Ensure this timeline item is on top
item.style.zIndex = '10000';
// Set positioning to keep the correct edge fixed (using pre-expansion position)
if (isAboveAxis) {
// Keep top edge fixed - set top position
label.style.bottom = 'auto';
label.style.top = currentTop + 'px';
} else {
// Keep bottom edge fixed - set bottom position
label.style.top = 'auto';
label.style.bottom = currentBottom + 'px';
}
} else {
// Contracting - remove expanded class first
label.classList.remove('expanded');
// Wait for CSS transition to start, then restore compact content
setTimeout(() => {
// Restore compact content
label.innerHTML = `
<div class="model-title">${label.dataset.modelName}</div>
<div class="timeline-date">${label.dataset.modelDate}</div>
`;
// Reset positioning after content change
setTimeout(() => {
label.style.top = '';
label.style.bottom = '';
item.style.zIndex = ''; // Reset z-index too
}, 50); // Small delay to ensure content is set
}, 50); // Small delay to start transition
}
});
const releaseInfo = model.release_date && model.release_date !== 'None' ?
`\\nReleased: ${model.release_date}` : '\\nRelease date: Unknown';
const modalityInfo = model.modality_name ? `\\nModality: ${model.modality_name}` : '';
item.title = `${modelName}\\nAdded: ${modelDate}${releaseInfo}${modalityInfo}`;
// Add dot, connector line, and label
item.appendChild(dot);
item.appendChild(connector);
item.appendChild(label);
timeline.appendChild(item);
});
// Only focus on the end for initial load, not for zoom operations
if (!preservePosition) {
// Initial load - focus on the end (most recent models)
timelineOffset = Math.min(0, containerWidth - timelineWidth);
}
updateTimelinePosition();
setupNavigation();
updateZoomIndicator();
}
function updateZoomIndicator() {
const zoomIndicator = document.getElementById('zoomLevel');
zoomIndicator.textContent = Math.round(zoomLevel * 100) + '%';
}
function zoomIn() {
if (zoomLevel < maxZoom) {
// Store current state for smooth transition
const oldTimelineWidth = timelineWidth;
const currentCenterX = -timelineOffset + (containerWidth / 2);
const centerRatio = currentCenterX / oldTimelineWidth;
// Update zoom level
zoomLevel = Math.min(maxZoom, zoomLevel * 1.2);
// Calculate new dimensions
const baseSpacing = 80;
const newActualSpacing = baseSpacing * zoomLevel;
const newTimelineWidth = Math.max(containerWidth, currentModels.length * newActualSpacing + 200);
// Calculate new position to preserve center
const newCenterX = centerRatio * newTimelineWidth;
const targetOffset = -(newCenterX - (containerWidth / 2));
// Apply smooth transition by setting target position
timelineOffset = targetOffset;
// Re-render with smooth transition
renderTimeline(currentModels, true);
}
}
function zoomOut() {
if (zoomLevel > minZoom) {
// Store current state for smooth transition
const oldTimelineWidth = timelineWidth;
const currentCenterX = -timelineOffset + (containerWidth / 2);
const centerRatio = currentCenterX / oldTimelineWidth;
// Update zoom level
zoomLevel = Math.max(minZoom, zoomLevel / 1.2);
// Calculate new dimensions
const baseSpacing = 80;
const newActualSpacing = baseSpacing * zoomLevel;
const newTimelineWidth = Math.max(containerWidth, currentModels.length * newActualSpacing + 200);
// Calculate new position to preserve center
const newCenterX = centerRatio * newTimelineWidth;
const targetOffset = -(newCenterX - (containerWidth / 2));
// Apply smooth transition by setting target position
timelineOffset = targetOffset;
// Re-render with smooth transition
renderTimeline(currentModels, true);
}
}
function addDateMarkers(models, spacing) {
if (models.length === 0) return;
const timeline = document.getElementById('timeline');
// Sort models by date to ensure proper chronological order
const sortedModels = [...models].sort((a, b) =>
new Date(a.transformers_date) - new Date(b.transformers_date)
);
// Get date range
const startDate = new Date(sortedModels[0].transformers_date);
const endDate = new Date(sortedModels[sortedModels.length - 1].transformers_date);
// Determine marker granularity based on spacing and zoom
const totalSpan = spacing * models.length;
const pixelsPerDay = totalSpan / ((endDate - startDate) / (1000 * 60 * 60 * 24));
let markerType, increment, format;
if (pixelsPerDay > 4) {
// Very zoomed in - show months
markerType = 'month';
increment = 1;
format = (date) => date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
} else if (pixelsPerDay > 1.5) {
// Medium zoom - show quarters
markerType = 'quarter';
increment = 3;
format = (date) => {
const quarter = Math.floor(date.getMonth() / 3) + 1;
return `Q${quarter} ${date.getFullYear()}`;
};
} else {
// Zoomed out - show years only
markerType = 'year';
increment = 12;
format = (date) => date.getFullYear().toString();
}
// Generate markers based on actual model positions
let currentDate = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
// Round to appropriate boundary
if (markerType === 'quarter') {
currentDate.setMonth(Math.floor(currentDate.getMonth() / 3) * 3);
} else if (markerType === 'year') {
currentDate.setMonth(0);
}
while (currentDate <= endDate) {
// Find the boundary position between models before and after this date
let boundaryPosition = null;
for (let i = 0; i < sortedModels.length - 1; i++) {
const currentModelDate = new Date(sortedModels[i].transformers_date);
const nextModelDate = new Date(sortedModels[i + 1].transformers_date);
// Check if the marker date falls between these two models
// The marker should appear where the time period actually starts
if (currentModelDate < currentDate && currentDate <= nextModelDate) {
// Position the marker between these two models, then move left by half segment
const currentPos = i * spacing + 100;
const nextPos = (i + 1) * spacing + 100;
const midpoint = (currentPos + nextPos) / 2;
boundaryPosition = midpoint - (spacing / 2); // Move left by half segment length
break;
}
}
// If no boundary found (e.g., date is before first model or after last model)
if (boundaryPosition === null) {
if (currentDate < new Date(sortedModels[0].transformers_date)) {
// Date is before first model
boundaryPosition = 50; // Position before first model
} else {
// Date is after last model
boundaryPosition = (sortedModels.length - 1) * spacing + 150; // Position after last model
}
}
const position = boundaryPosition;
// Create marker only if it's within visible range and not too close to adjacent markers
const existingMarkers = timeline.querySelectorAll('.date-marker');
let tooClose = false;
existingMarkers.forEach(existing => {
const existingPos = parseFloat(existing.style.left);
if (Math.abs(existingPos - position) < 120) { // Minimum spacing between markers
tooClose = true;
}
});
if (!tooClose) {
const marker = document.createElement('div');
marker.className = `date-marker ${markerType}`;
marker.style.left = position + 'px';
// Create label
const label = document.createElement('div');
label.className = 'date-label';
label.textContent = format(currentDate);
marker.appendChild(label);
// Create vertical line
const line = document.createElement('div');
line.style.position = 'absolute';
line.style.top = '0px';
line.style.bottom = '0px';
line.style.left = '0px';
line.style.width = '2px';
line.style.background = '#9ca3af';
line.style.opacity = '0.6';
line.style.zIndex = '1';
if (markerType === 'year') {
line.style.width = '3px';
line.style.background = '#6b7280';
line.style.opacity = '0.8';
}
marker.appendChild(line);
timeline.appendChild(marker);
}
// Move to next marker
currentDate.setMonth(currentDate.getMonth() + increment);
}
}
function updateTimelinePosition() {
const timeline = document.getElementById('timeline');
timeline.style.transform = `translateX(${timelineOffset}px)`;
// Update navigation buttons
const navLeft = document.getElementById('navLeft');
const navRight = document.getElementById('navRight');
navLeft.style.opacity = timelineOffset >= 0 ? '0.3' : '1';
navRight.style.opacity = timelineOffset <= containerWidth - timelineWidth ? '0.3' : '1';
}
function setupNavigation() {
const navLeft = document.getElementById('navLeft');
const navRight = document.getElementById('navRight');
const timelineWrapper = document.querySelector('.timeline-wrapper');
const timelineScroll = document.getElementById('timelineScroll');
const zoomInBtn = document.getElementById('zoomIn');
const zoomOutBtn = document.getElementById('zoomOut');
// Arrow navigation
navLeft.onclick = () => {
if (timelineOffset < 0) {
timelineOffset = Math.min(0, timelineOffset + containerWidth * 0.8);
updateTimelinePosition();
}
};
navRight.onclick = () => {
const maxOffset = containerWidth - timelineWidth;
if (timelineOffset > maxOffset) {
timelineOffset = Math.max(maxOffset, timelineOffset - containerWidth * 0.8);
updateTimelinePosition();
}
};
// Zoom controls
zoomInBtn.onclick = zoomIn;
zoomOutBtn.onclick = zoomOut;
// Drag functionality - works on entire timeline area
timelineWrapper.onmousedown = (e) => {
// Ignore clicks on navigation arrows and zoom controls
if (e.target.closest('.nav-arrow') || e.target.closest('.zoom-controls')) {
return;
}
// Ignore clicks inside expanded cards to allow text selection
if (e.target.closest('.timeline-label.expanded')) {
return;
}
isDragging = true;
startX = e.clientX;
startOffset = timelineOffset;
timelineWrapper.style.cursor = 'grabbing';
// Remove transition during drag for immediate response
const timeline = document.getElementById('timeline');
timeline.style.transition = 'none';
e.preventDefault(); // Prevent text selection
};
document.onmousemove = (e) => {
if (!isDragging) return;
// Increased sensitivity - 1.3x multiplier for more responsive feel
const deltaX = (e.clientX - startX) * 1.3;
const newOffset = startOffset + deltaX;
const maxOffset = containerWidth - timelineWidth;
timelineOffset = Math.max(maxOffset, Math.min(0, newOffset));
updateTimelinePosition();
};
document.onmouseup = () => {
if (isDragging) {
isDragging = false;
timelineWrapper.style.cursor = 'grab';
// Restore transition after drag
const timeline = document.getElementById('timeline');
timeline.style.transition = 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
}
};
// Touch support for mobile - enhanced responsiveness
timelineWrapper.ontouchstart = (e) => {
// Ignore touches on navigation arrows and zoom controls
if (e.target.closest('.nav-arrow') || e.target.closest('.zoom-controls')) {
return;
}
// Ignore touches inside expanded cards to allow text selection
if (e.target.closest('.timeline-label.expanded')) {
return;
}
isDragging = true;
startX = e.touches[0].clientX;
startOffset = timelineOffset;
// Remove transition during touch drag
const timeline = document.getElementById('timeline');
timeline.style.transition = 'none';
};
timelineWrapper.ontouchmove = (e) => {
if (!isDragging) return;
e.preventDefault();
// Increased sensitivity for touch as well
const deltaX = (e.touches[0].clientX - startX) * 1.3;
const newOffset = startOffset + deltaX;
const maxOffset = containerWidth - timelineWidth;
timelineOffset = Math.max(maxOffset, Math.min(0, newOffset));
updateTimelinePosition();
};
timelineWrapper.ontouchend = () => {
if (isDragging) {
isDragging = false;
// Restore transition after touch drag
const timeline = document.getElementById('timeline');
timeline.style.transition = 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
}
};
// Keyboard navigation and zoom
document.onkeydown = (e) => {
if (e.key === 'ArrowLeft') {
navLeft.onclick();
} else if (e.key === 'ArrowRight') {
navRight.onclick();
} else if (e.key === '+' || e.key === '=') {
zoomIn();
} else if (e.key === '-' || e.key === '_') {
zoomOut();
}
};
// Mouse wheel zoom - works anywhere in timeline area
timelineWrapper.onwheel = (e) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
}
};
}
function checkAllModalities() {
document.querySelectorAll('.modality-checkbox input').forEach(checkbox => {
checkbox.checked = true;
checkbox.parentElement.classList.add('checked');
});
// Auto-refresh timeline
loadTimeline();
}
function clearAllModalities() {
document.querySelectorAll('.modality-checkbox input').forEach(checkbox => {
checkbox.checked = false;
checkbox.parentElement.classList.remove('checked');
});
// Auto-refresh timeline
loadTimeline();
}
// Task filtering functions
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const data = await response.json();
if (!data.success) {
console.error('Failed to load tasks:', data.error);
return;
}
const taskFilters = document.getElementById('taskFilters');
taskFilters.innerHTML = '';
data.tasks.forEach(task => {
const checkboxContainer = document.createElement('div');
checkboxContainer.className = 'task-checkbox';
checkboxContainer.style.color = task.color;
checkboxContainer.style.setProperty('--task-color', task.color);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = task.key;
checkbox.id = `task-${task.key}`;
checkbox.checked = false; // Start with all tasks unchecked
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
if (checkbox.checked) {
checkboxContainer.classList.add('checked');
} else {
checkboxContainer.classList.remove('checked');
}
// Auto-refresh timeline
loadTimeline();
});
const label = document.createElement('label');
label.htmlFor = `task-${task.key}`;
label.textContent = task.name;
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(label);
// Add click handler with auto-refresh
checkboxContainer.addEventListener('click', (e) => {
if (e.target.type !== 'checkbox') {
checkbox.checked = !checkbox.checked;
}
checkboxContainer.classList.toggle('checked', checkbox.checked);
// Auto-refresh timeline when task filter changes
loadTimeline();
});
taskFilters.appendChild(checkboxContainer);
});
console.log('✅ Loaded', data.tasks.length, 'task filters');
} catch (error) {
console.error('Error loading tasks:', error);
}
}
function checkAllTasks() {
document.querySelectorAll('.task-checkbox input').forEach(checkbox => {
checkbox.checked = true;
checkbox.parentElement.classList.add('checked');
});
// Auto-refresh timeline
loadTimeline();
}
function clearAllTasks() {
document.querySelectorAll('.task-checkbox input').forEach(checkbox => {
checkbox.checked = false;
checkbox.parentElement.classList.remove('checked');
});
// Auto-refresh timeline
loadTimeline();
}
// Optional function to clear just date filters if needed
function clearDateFilters() {
document.getElementById('startDate').value = '';
document.getElementById('endDate').value = '';
// Auto-refresh will be triggered by the date change events
loadTimeline();
}
// Function to get task color matching the filter colors
function getTaskColor(taskKey) {
const taskColors = {
"text-generation": "#6366f1",
"text-classification": "#8b5cf6",
"token-classification": "#a855f7",
"question-answering": "#c084fc",
"fill-mask": "#d8b4fe",
"text2text-generation": "#e879f9",
"image-classification": "#06b6d4",
"object-detection": "#0891b2",
"image-segmentation": "#0e7490",
"semantic-segmentation": "#155e75",
"instance-segmentation": "#164e63",
"universal-segmentation": "#1e40af",
"depth-estimation": "#1d4ed8",
"zero-shot-image-classification": "#2563eb",
"zero-shot-object-detection": "#3b82f6",
"image-to-image": "#60a5fa",
"mask-generation": "#93c5fd",
"image-to-text": "#10b981",
"image-text-to-text": "#059669",
"visual-question-answering": "#047857",
"document-question-answering": "#065f46",
"table-question-answering": "#064e3b",
"video-classification": "#dc2626",
"audio-classification": "#ea580c",
"text-to-audio": "#f97316",
"time-series-classification": "#84cc16",
"time-series-regression": "#65a30d",
"time-series-prediction": "#4d7c0f"
};
return taskColors[taskKey] || "#6b7280"; // Default gray for unmapped tasks
}
async function loadModalities() {
try {
const response = await fetch('/api/modalities');
const data = await response.json();
if (!data.success) {
console.error('Failed to load modalities:', data.error);
return;
}
const modalityFilters = document.getElementById('modalityFilters');
modalityFilters.innerHTML = '';
data.modalities.forEach(modality => {
const checkboxContainer = document.createElement('div');
checkboxContainer.className = 'modality-checkbox';
checkboxContainer.style.color = modality.color;
checkboxContainer.style.setProperty('--modality-color', modality.color);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = modality.key;
checkbox.id = `modality-${modality.key}`;
checkbox.checked = true; // All modalities selected by default
const label = document.createElement('label');
label.htmlFor = `modality-${modality.key}`;
label.textContent = modality.name;
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(label);
// Add click handler with auto-refresh
checkboxContainer.addEventListener('click', (e) => {
if (e.target.type !== 'checkbox') {
checkbox.checked = !checkbox.checked;
}
checkboxContainer.classList.toggle('checked', checkbox.checked);
// Auto-refresh timeline when modality filter changes
loadTimeline();
});
// Set initial state
checkboxContainer.classList.add('checked');
modalityFilters.appendChild(checkboxContainer);
});
} catch (error) {
console.error('Error loading modalities:', error);
}
}
// Window resize handler
window.addEventListener('resize', () => {
if (currentModels.length > 0) {
renderTimeline(currentModels);
}
});
document.addEventListener('DOMContentLoaded', async () => {
await loadModalities();
await loadTasks();
loadTimeline();
// Add auto-refresh for date inputs
document.getElementById('startDate').addEventListener('change', loadTimeline);
document.getElementById('endDate').addEventListener('change', loadTimeline);
// Close expanded cards when clicking outside
document.addEventListener('click', (e) => {
// Don't close if clicking inside an expanded card
if (e.target.closest('.timeline-label.expanded')) {
return;
}
// Don't close if clicking on a timeline label (non-expanded)
if (e.target.closest('.timeline-label:not(.expanded)')) {
return;
}
// Close all expanded cards
document.querySelectorAll('.timeline-label.expanded').forEach(label => {
label.classList.remove('expanded');
// Restore compact content
setTimeout(() => {
label.innerHTML = `
<div class="model-title">${label.dataset.modelName}</div>
<div class="timeline-date">${label.dataset.modelDate}</div>
`;
// Reset positioning after content change
setTimeout(() => {
label.style.top = '';
label.style.bottom = '';
label.parentElement.style.zIndex = '';
}, 50);
}, 50);
});
});
// Initialize filters collapsible panel
initFiltersCollapsible();
});
function initFiltersCollapsible() {
const panel = document.getElementById('filtersPanel');
const btn = document.getElementById('toggleFilters');
if (!panel || !btn) return;
// Restore persisted state, default to collapsed
const saved = localStorage.getItem('filtersCollapsed');
if (saved === 'false') {
// Set natural max-height to allow animation on first collapse
panel.style.maxHeight = panel.scrollHeight + 'px';
btn.textContent = '▾';
} else {
// set partial height (~2 rows) when collapsed (default state)
const partial = 74; // pixels to show when collapsed
panel.style.maxHeight = partial + 'px';
panel.classList.add('collapsed');
btn.textContent = '▸';
}
}
function toggleFilters() {
const panel = document.getElementById('filtersPanel');
const btn = document.getElementById('toggleFilters');
if (!panel || !btn) return;
const isCollapsed = panel.classList.contains('collapsed');
if (isCollapsed) {
// expand to full content height
panel.classList.remove('collapsed');
panel.style.maxHeight = panel.scrollHeight + 'px';
btn.textContent = '▾';
localStorage.setItem('filtersCollapsed', 'false');
} else {
// collapse to partial height (show a hint of content)
panel.style.maxHeight = panel.scrollHeight + 'px'; // set current height
void panel.offsetHeight; // reflow
panel.classList.add('collapsed');
const partial = 74; // pixels to show when collapsed
panel.style.maxHeight = partial + 'px';
btn.textContent = '▸';
localStorage.setItem('filtersCollapsed', 'true');
}
}
</script>
</body>
</html>"""
with open(os.path.join(template_dir, "timeline.html"), "w", encoding="utf-8") as f:
f.write(html_content)
def open_browser():
"""Open the browser after a short delay."""
time.sleep(1.5)
webbrowser.open("http://localhost:5000")
def main():
"""Main function to run the timeline app."""
print("🤗 Transformers Models Timeline")
print("=" * 50)
# Create templates
create_timeline_template()
# Check if docs directory exists
if not os.path.exists(docs_dir):
print(f"❌ Error: Documentation directory not found at {docs_dir}")
print("Please update the 'docs_dir' variable in the script.")
return
# Parse models to check if any are found
models = parser.parse_all_model_dates()
if not models:
print(f"⚠️ Warning: No models found with release dates in {docs_dir}")
else:
print(f"✅ Found {len(models)} models with release dates")
# Run Flask app
try:
app.run(host="0.0.0.0", port=7860, debug=False)
except KeyboardInterrupt:
print("\n👋 Timeline server stopped")
if __name__ == "__main__":
main()