eustlb
initial commit
c4a1067
raw
history blame
140 kB
#!/usr/bin/env python3
"""
Beautiful custom timeline visualization for Transformers models using Flask.
"""
import glob
import json
import os
import re
import subprocess
import sys
import time
import webbrowser
from datetime import datetime
from pathlib import Path
from typing import Optional
from flask import Flask, jsonify, render_template, request
import transformers
try:
import yaml # type: ignore
except Exception: # pragma: no cover
yaml = None
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)
# Parse modalities dynamically; no fallback to static definitions
parsed_modalities = self._parse_modalities_from_toctree()
if not parsed_modalities:
raise RuntimeError("Failed to parse modalities from docs toctree (_toctree.yml)")
self.modalities = parsed_modalities
def _parse_modalities_from_toctree(self) -> Optional[dict[str, dict[str, object]]]:
"""Parse model modalities and slugs from docs/source/en/_toctree.yml.
Returns a dict with the same schema as self.modalities or None on failure.
"""
# Compute toctree path relative to provided docs_dir
toctree_path = os.path.join(self.docs_dir, "..", "_toctree.yml")
if not os.path.isfile(toctree_path):
return None
if yaml is None:
return None
with open(toctree_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, list):
return None
# Locate API -> Models
api_top = None
for entry in data:
if isinstance(entry, dict) and entry.get("title") == "API" and entry.get("sections"):
api_top = entry
break
if api_top is None:
def _dfs_find_api(node):
if isinstance(node, dict) and node.get("title") == "API" and node.get("sections"):
return node
if isinstance(node, dict):
for v in node.values():
found = _dfs_find_api(v)
if found is not None:
return found
if isinstance(node, list):
for v in node:
found = _dfs_find_api(v)
if found is not None:
return found
return None
api_top = _dfs_find_api(data)
if api_top is None:
return None
models_top = None
for sec in api_top.get("sections", []):
if isinstance(sec, dict) and sec.get("title") == "Models" and sec.get("sections"):
models_top = sec
break
if models_top is None:
def _dfs_find_models(node):
if isinstance(node, dict) and node.get("title") == "Models" and node.get("sections"):
return node
if isinstance(node, dict):
for v in node.values():
found = _dfs_find_models(v)
if found is not None:
return found
if isinstance(node, list):
for v in node:
found = _dfs_find_models(v)
if found is not None:
return found
return None
models_top = _dfs_find_models(api_top)
if models_top is None:
return None
# Helper to extract slugs from a section like "Text models"
def extract_model_slugs(section_title: str) -> list[str]:
result: list[str] = []
for sec in models_top.get("sections", []):
if isinstance(sec, dict) and sec.get("title") == section_title:
# Items may be nested under sections -> sections -> list of {local: model_doc/<slug>, title: ...}
nested = sec.get("sections") or []
for sub in nested:
if not isinstance(sub, dict):
continue
# Direct list:
if "local" in sub:
local = sub.get("local")
if isinstance(local, str) and local.startswith("model_doc/"):
result.append(local.split("/", 1)[1])
# Or deeper nesting
for leaf in sub.get("sections", []) if isinstance(sub.get("sections"), list) else []:
local = leaf.get("local")
if isinstance(local, str) and local.startswith("model_doc/"):
result.append(local.split("/", 1)[1])
return result
text_models = extract_model_slugs("Text models")
vision_models = extract_model_slugs("Vision models")
audio_models = extract_model_slugs("Audio models")
video_models = extract_model_slugs("Video models")
multimodal_models = extract_model_slugs("Multimodal models")
rl_models = extract_model_slugs("Reinforcement learning models")
ts_models = extract_model_slugs("Time series models")
graph_models = extract_model_slugs("Graph models")
# Basic validation: require at least some categories to be non-empty
if not any([text_models, vision_models, audio_models, video_models, multimodal_models]):
return None
# Preserve existing names and colors
return {
"text": {"name": "Text Models", "color": "#F59E0B", "models": text_models},
"vision": {"name": "Vision Models", "color": "#06B6D4", "models": vision_models},
"audio": {"name": "Audio Models", "color": "#8B5CF6", "models": audio_models},
"video": {"name": "Video Models", "color": "#EC4899", "models": video_models},
"multimodal": {"name": "Multimodal Models", "color": "#10B981", "models": multimodal_models},
"reinforcement": {"name": "Reinforcement Learning", "color": "#EF4444", "models": rl_models},
"timeseries": {"name": "Time Series Models", "color": "#F97316", "models": ts_models},
"graph": {"name": "Graph Models", "color": "#6B7280", "models": graph_models},
}
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...")
subprocess.run(["git", "clone", "https://github.com/huggingface/transformers.git", repo_dir], check=True)
subprocess.run(["pip", "install", "-e", repo_dir], check=True)
else:
# fetch the latest changes and pull them
subprocess.run(["git", "fetch", "--all", "--prune"], cwd=repo_dir, check=True)
subprocess.run(["git", "pull", "origin", "main"], cwd=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
@app.route("/api/search-files")
def search_files():
"""API endpoint to search for modeling files containing a specific string."""
search_string = request.args.get("query", "")
if not search_string:
return jsonify({"success": False, "error": "No search query provided"}), 400
try:
# Get model to date mapping from parsed models
models = parser.parse_all_model_dates()
model_to_date = {m["model_name"]: m["transformers_date"] for m in models}
# Path to modeling files
modeling_dir = os.path.join(repo_dir, 'src', 'transformers', 'models')
if not os.path.exists(modeling_dir):
return jsonify({"success": False, "error": f"Models directory not found at {modeling_dir}"}), 500
# Find matching files
matching_files = []
for model_name, date_str in model_to_date.items():
# Try multiple file naming patterns
possible_files = [
os.path.join(modeling_dir, model_name, f'modeling_{model_name}.py'),
os.path.join(modeling_dir, model_name.replace('-', '_'), f'modeling_{model_name.replace("-", "_")}.py'),
os.path.join(modeling_dir, model_name, 'modeling.py'),
]
for modeling_file in possible_files:
if os.path.exists(modeling_file):
try:
with open(modeling_file, 'r', encoding='utf-8') as f:
content = f.read()
if search_string in content:
# Count occurrences
count = content.count(search_string)
# Get file size
file_size = os.path.getsize(modeling_file) / 1024 # KB
matching_files.append({
"model_name": model_name,
"date": date_str,
"file_path": os.path.relpath(modeling_file, repo_dir),
"occurrences": count,
"size_kb": round(file_size, 2)
})
break # Found a match, no need to check other patterns
except Exception as e:
print(f"Error reading {modeling_file}: {e}")
continue
# Sort by date (most recent first)
matching_files.sort(key=lambda x: x["date"], reverse=True)
# Return top 10
top_10 = matching_files[:10]
return jsonify({
"success": True,
"query": search_string,
"total_matches": len(matching_files),
"results": top_10
})
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;
transition: all 0.3s ease;
}
/* Dark mode body */
[data-theme="dark"] body {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
color: #f9fafb;
}
.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;
transition: all 0.3s ease;
}
/* Dark mode styles */
[data-theme="dark"] .header {
background: rgba(17, 24, 39, 0.9);
border-bottom: 1px solid rgba(55, 65, 81, 0.5);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.header h1 {
font-size: 1.6rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 0.1rem;
transition: color 0.3s ease;
}
.header p {
color: #666;
font-size: 0.85rem;
margin: 0;
transition: color 0.3s ease;
}
/* Dark mode header text */
[data-theme="dark"] .header h1 {
color: #f9fafb;
}
[data-theme="dark"] .header p {
color: #d1d5db;
}
/* Header layout */
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-text {
flex: 1;
}
/* Theme toggle button */
.theme-toggle {
background: rgba(255, 255, 255, 0.9);
border: 2px solid #e2e8f0;
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1.2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.theme-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: #d97706;
}
.theme-toggle:active {
transform: translateY(0);
}
.theme-icon {
transition: transform 0.3s ease;
}
/* Dark mode theme toggle */
[data-theme="dark"] .theme-toggle {
background: rgba(17, 24, 39, 0.9);
border-color: #4b5563;
color: #f9fafb;
}
[data-theme="dark"] .theme-toggle:hover {
border-color: #f59e0b;
}
.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);
}
/* Dark mode controls toggle */
[data-theme="dark"] .controls-toggle {
background: #374151;
border: 1px solid #4b5563;
color: #f9fafb;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.controls-toggle:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0,0,0,0.1);
}
/* Dark mode controls toggle hover */
[data-theme="dark"] .controls-toggle:hover {
background: #4b5563;
border-color: #6b7280;
box-shadow: 0 4px 14px rgba(0,0,0,0.4);
}
.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, background 0.3s ease, box-shadow 0.3s ease;
position: relative;
padding-left: 2.4rem; /* space for the left toggle button */
max-height: 74px; /* default collapsed height */
}
/* Dark mode controls */
[data-theme="dark"] .controls {
background: rgba(17, 24, 39, 0.9);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.controls.collapsed {
opacity: 1;
max-height: 74px !important;
}
/* 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;
}
/* Dark mode blur fade */
[data-theme="dark"] .controls.collapsed::after {
background: linear-gradient(to bottom, rgba(17,24,39,0), rgba(17,24,39,0.94) 60%, rgba(17,24,39,1));
}
.input-group {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 120px;
}
.input-group label {
font-weight: 500;
color: #4a5568;
font-size: 0.75rem;
transition: color 0.3s ease;
}
/* Dark mode labels */
[data-theme="dark"] .input-group label {
color: #d1d5db;
}
.input-group input {
padding: 0.35rem 0.65rem;
border: 1px solid #e2e8f0;
border-radius: 5px;
font-size: 0.75rem;
transition: all 0.2s ease;
background: white;
}
/* Dark mode input elements */
[data-theme="dark"] .input-group input {
background: #374151;
border-color: #4b5563;
color: #f9fafb;
}
[data-theme="dark"] .input-group input:focus {
border-color: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
}
.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 */
display: flex;
flex-direction: column;
}
.modality-filters {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.2rem;
flex: 1;
}
.modality-buttons {
display: flex;
gap: 0.3rem;
margin-top: 0.3rem;
margin-bottom: 0.5rem;
align-items: flex-end;
}
.task-group {
flex: 78%;
min-width: 520px;
display: flex;
flex-direction: column;
}
.task-filters {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.2rem;
flex: 1;
}
.task-buttons {
display: flex;
gap: 0.3rem;
margin-top: 0.3rem;
margin-bottom: 0.5rem;
align-items: flex-end;
}
.btn-small {
padding: 0.45rem 0.65rem;
font-size: 0.7rem;
margin-top: 0;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
}
.modality-checkbox {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.55rem;
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.7rem;
font-weight: 500;
position: relative;
overflow: hidden;
}
/* Dark mode modality checkboxes */
[data-theme="dark"] .modality-checkbox {
background: rgba(17, 24, 39, 0.9);
border: 1px solid rgba(55, 65, 81, 0.5);
}
.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;
}
/* Dark mode checked modality checkbox cards */
[data-theme="dark"] .modality-checkbox.checked {
background: rgba(31, 41, 55, 0.95);
border-color: currentColor;
}
.modality-checkbox.checked::before {
opacity: 0.08;
}
.modality-checkbox input[type="checkbox"] {
appearance: none;
width: 17px;
height: 17px;
border: 2px solid var(--modality-color, #8B5CF6);
border-radius: 4px;
position: relative;
margin: 0;
background: white;
transition: all 0.2s ease;
flex-shrink: 0;
}
/* Dark mode unchecked checkboxes */
[data-theme="dark"] .modality-checkbox input[type="checkbox"] {
background: #374151;
border-color: var(--modality-color, #8B5CF6);
}
.modality-checkbox input[type="checkbox"]:checked {
background: var(--modality-color, #8B5CF6);
border-color: var(--modality-color, #8B5CF6);
}
/* Dark mode checked checkboxes */
[data-theme="dark"] .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;
transition: color 0.3s ease;
}
/* Dark mode modality checkbox labels */
[data-theme="dark"] .modality-checkbox label {
color: #f9fafb;
}
/* Task filter styles (mirroring modality styles) */
.task-checkbox {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.55rem;
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.7rem;
font-weight: 500;
position: relative;
overflow: hidden;
}
/* Dark mode task checkboxes */
[data-theme="dark"] .task-checkbox {
background: rgba(17, 24, 39, 0.9);
border: 1px solid rgba(55, 65, 81, 0.5);
}
.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;
}
/* Dark mode checked task checkbox cards */
[data-theme="dark"] .task-checkbox.checked {
background: rgba(31, 41, 55, 0.95);
border-color: currentColor;
}
.task-checkbox.checked::before {
opacity: 0.08;
}
.task-checkbox input[type="checkbox"] {
appearance: none;
width: 17px;
height: 17px;
border: 2px solid var(--task-color, #6366f1);
border-radius: 4px;
background: white;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
}
/* Dark mode unchecked task checkboxes */
[data-theme="dark"] .task-checkbox input[type="checkbox"] {
background: #374151;
border-color: var(--task-color, #6366f1);
}
.task-checkbox input[type="checkbox"]:checked {
background: var(--task-color, #6366f1);
border-color: var(--task-color, #6366f1);
}
/* Dark mode checked task checkboxes */
[data-theme="dark"] .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;
transition: color 0.3s ease;
}
/* Dark mode task checkbox labels */
[data-theme="dark"] .task-checkbox label {
color: #f9fafb;
}
.modality-checkbox input[type="checkbox"]:not(:checked) {
background: white;
border-color: var(--modality-color, #8B5CF6);
}
/* Dark mode unchecked checkboxes override */
[data-theme="dark"] .modality-checkbox input[type="checkbox"]:not(:checked) {
background: #374151;
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;
transition: all 0.3s ease;
}
/* Dark mode secondary buttons */
[data-theme="dark"] .btn-secondary {
background: #374151;
color: #f9fafb;
border: 2px solid #4b5563;
}
[data-theme="dark"] .btn-secondary:hover {
background: #4b5563;
border-color: #6b7280;
}
.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);
transition: all 0.3s ease;
}
/* Dark mode timeline container */
[data-theme="dark"] .timeline-container {
background: rgba(17, 24, 39, 0.95);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.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;
}
/* Dark mode navigation arrows */
[data-theme="dark"] .nav-arrow {
background: rgba(17, 24, 39, 0.9);
color: #f59e0b;
border: 2px solid rgba(245, 158, 11, 0.2);
}
.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);
}
/* Dark mode navigation arrow hover */
[data-theme="dark"] .nav-arrow:hover {
background: rgba(245, 158, 11, 0.1);
border-color: #f59e0b;
box-shadow: 0 4px 16px rgba(245, 158, 11, 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: 6px;
z-index: 20;
}
.zoom-btn {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.9);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.9rem;
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;
}
/* Dark mode zoom buttons */
[data-theme="dark"] .zoom-btn {
background: rgba(17, 24, 39, 0.9);
border: 2px solid rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.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);
}
/* Dark mode zoom button hover */
[data-theme="dark"] .zoom-btn:hover {
background: rgba(245, 158, 11, 0.1);
border-color: #f59e0b;
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.3);
}
.zoom-btn:active {
transform: scale(0.95);
}
.zoom-indicator {
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
padding: 2px 6px;
font-size: 0.7rem;
color: #d97706;
font-weight: 500;
border: 1px solid rgba(217, 119, 6, 0.2);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 28px;
}
/* Dark mode zoom indicator */
[data-theme="dark"] .zoom-indicator {
background: rgba(17, 24, 39, 0.95);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 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 */
transition: all 0.3s ease;
}
/* Dark mode date markers */
[data-theme="dark"] .date-marker {
background: #6b7280;
opacity: 0.9;
}
.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);
transition: all 0.3s ease;
}
/* Dark mode date labels */
[data-theme="dark"] .date-label {
color: #d1d5db;
background: rgba(17, 24, 39, 0.8);
border: 1px solid rgba(75, 85, 99, 0.4);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.date-marker.year {
opacity: 0.9;
background: #6b7280;
width: 3px;
}
/* Dark mode year markers */
[data-theme="dark"] .date-marker.year {
background: #9ca3af;
opacity: 1.0;
}
.date-marker.year .date-label {
font-weight: 600;
color: #4b5563;
background: rgba(255, 255, 255, 0.9);
font-size: 0.8rem;
border-color: #9ca3af;
}
/* Dark mode year date labels */
[data-theme="dark"] .date-marker.year .date-label {
color: #f9fafb;
background: rgba(17, 24, 39, 0.9);
border-color: #6b7280;
}
.date-marker.quarter {
opacity: 0.7;
}
.date-marker.month {
opacity: 0.8;
}
/* Dark mode quarter and month markers */
[data-theme="dark"] .date-marker.quarter {
opacity: 0.8;
}
[data-theme="dark"] .date-marker.month {
opacity: 0.9;
}
.timeline-dot {
width: 19px;
height: 19px;
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: 110px;
max-width: 170px;
padding: 0.5rem 0.7rem;
background: white;
border-radius: 6px;
font-size: 0.85rem;
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;
}
/* Dark mode timeline labels */
[data-theme="dark"] .timeline-label {
background: #374151;
border: 1px solid #4b5563;
color: #f9fafb;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4);
}
.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);
}
/* Dark mode timeline label hover */
[data-theme="dark"] .timeline-label:not(.expanded):hover {
background: rgba(55, 65, 81, 0.98);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.4),
0 0 4px var(--modality-color, #8B5CF6);
border-color: var(--modality-color, #8B5CF6);
}
/* Expanded card styles */
.timeline-label.expanded {
max-width: 550px !important;
min-width: 450px !important;
width: 550px !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.95rem;
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;
transition: color 0.3s ease;
}
/* Dark mode model title */
[data-theme="dark"] .timeline-label .model-title {
color: #f9fafb;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.timeline-label .model-description {
font-size: 0.8rem;
line-height: 1.4;
color: #4b5563;
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s ease;
position: relative;
}
/* Dark mode model description */
[data-theme="dark"] .timeline-label .model-description {
color: #d1d5db;
}
.timeline-label.expanded .model-description {
max-height: 175px !important;
height: 175px !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 {
position: relative;
bottom: 2px;
max-height: 175px !important;
height: 175px !important;
overflow-y: auto;
padding-right: 8px; /* Space for scrollbar */
}
.timeline-label .description-fade {
position: sticky;
bottom: 0px; /* Position at the very bottom of the container */
left: 0;
height: 15px;
background: linear-gradient(to bottom,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 50%,
rgba(255, 255, 255, 1) 95%);
rgba(255, 255, 255, 1) 100%);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 10;
}
/* Dark mode description fade */
[data-theme="dark"] .timeline-label .description-fade {
background: linear-gradient(to bottom,
rgba(55, 65, 81, 0) 0%,
rgba(55, 65, 81, 0.9) 50%,
rgba(55, 65, 81, 1) 95%);
rgba(55, 65, 81, 1) 100%);
}
.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;
transition: color 0.3s ease;
}
/* Dark mode markdown headers */
[data-theme="dark"] .timeline-label .model-description h1,
[data-theme="dark"] .timeline-label .model-description h2,
[data-theme="dark"] .timeline-label .model-description h3 {
color: #f9fafb;
}
.timeline-label .model-description h1 { font-size: 0.9rem; }
.timeline-label .model-description h2 { font-size: 0.85rem; }
.timeline-label .model-description h3 { font-size: 0.8rem; }
.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.7rem;
transition: all 0.3s ease;
}
/* Dark mode code */
[data-theme="dark"] .timeline-label .model-description code {
background: #374151;
color: #f9fafb;
}
.timeline-label .model-description strong {
font-weight: 600;
color: #1f2937;
transition: color 0.3s ease;
}
.timeline-label .model-description em {
font-style: italic;
}
/* Dark mode strong and em */
[data-theme="dark"] .timeline-label .model-description strong {
color: #f9fafb;
}
.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.8rem;
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.2rem;
text-align: center;
}
/* Dark mode learn more button */
[data-theme="dark"] .timeline-label .learn-more {
background: rgba(55, 65, 81, 0.8);
}
.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);
transition: all 0.3s ease;
}
/* Dark mode model tasks */
[data-theme="dark"] .model-tasks {
background: rgba(31, 41, 55, 0.8);
border: 1px solid rgba(55, 65, 81, 0.5);
}
.tasks-label {
font-size: 0.8rem;
font-weight: 600;
color: #4a5568;
margin-bottom: 0.3rem;
transition: color 0.3s ease;
}
/* Dark mode tasks label */
[data-theme="dark"] .tasks-label {
color: #d1d5db;
}
.tasks-list {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.task-badge {
display: inline-block;
padding: 0.25rem 0.45rem;
color: white;
border-radius: 4px;
font-size: 0.7rem;
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.75rem;
color: #9ca3af;
margin-top: 0.3rem;
font-weight: 500;
transition: color 0.3s ease;
}
/* Dark mode timeline date */
[data-theme="dark"] .timeline-date {
color: #9ca3af;
}
.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);
transition: all 0.3s ease;
}
/* Dark mode date controls */
[data-theme="dark"] .date-controls {
background: rgba(17, 24, 39, 0.95);
border: 1px solid rgba(55, 65, 81, 0.5);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.date-input-group {
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
}
.date-input-group label {
font-size: 0.75rem;
color: #4a5568;
font-weight: 600;
white-space: nowrap;
transition: color 0.3s ease;
}
/* Dark mode date labels */
[data-theme="dark"] .date-input-group label {
color: #d1d5db;
}
.date-input-group input {
padding: 0.45rem 0.65rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 0.8rem;
background: white;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 135px;
text-align: center;
font-weight: 500;
color: #374151;
}
/* Dark mode date inputs */
[data-theme="dark"] .date-input-group input {
background: #374151;
border-color: #4b5563;
color: #f9fafb;
}
[data-theme="dark"] .date-input-group input:hover {
border-color: #6b7280;
}
[data-theme="dark"] .date-input-group input:focus {
border-color: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
}
.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;
transition: all 0.3s ease;
}
/* Dark mode stat cards */
[data-theme="dark"] .stat-card {
background: rgba(17, 24, 39, 0.9);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.stat-number {
font-size: 1.2rem;
font-weight: 600;
color: #d97706;
display: block;
}
.stat-label {
color: #666;
font-size: 0.75rem;
margin-top: 0.15rem;
transition: color 0.3s ease;
}
/* Dark mode stat labels */
[data-theme="dark"] .stat-label {
color: #9ca3af;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 1.1rem;
color: #666;
transition: color 0.3s ease;
}
/* Dark mode loading */
[data-theme="dark"] .loading {
color: #9ca3af;
}
.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;
transition: all 0.3s ease;
}
/* Dark mode error */
[data-theme="dark"] .error {
color: #fca5a5;
background: #7f1d1d;
}
/* Search Section Styles */
.search-wrapper {
margin: 0.5rem 2rem 0.5rem 2rem;
}
.search-container {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
padding: 1rem 1.5rem;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
[data-theme="dark"] .search-container {
background: rgba(17, 24, 39, 0.9);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.search-header h3 {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 0.3rem;
transition: color 0.3s ease;
}
[data-theme="dark"] .search-header h3 {
color: #f9fafb;
}
.search-header p {
font-size: 0.85rem;
color: #666;
margin-bottom: 1rem;
transition: color 0.3s ease;
}
[data-theme="dark"] .search-header p {
color: #d1d5db;
}
.search-controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
align-items: center;
}
#searchInput {
flex: 1;
padding: 0.6rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 0.9rem;
transition: all 0.3s ease;
background: white;
}
[data-theme="dark"] #searchInput {
background: #374151;
border-color: #4b5563;
color: #f9fafb;
}
#searchInput:focus {
outline: none;
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.1);
}
[data-theme="dark"] #searchInput:focus {
border-color: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
}
.search-results {
min-height: 50px;
max-height: 400px;
overflow-y: auto;
}
.search-results::-webkit-scrollbar {
width: 8px;
}
.search-results::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
[data-theme="dark"] .search-results::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.search-results::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 4px;
}
[data-theme="dark"] .search-results::-webkit-scrollbar-thumb {
background: #4b5563;
}
.search-results::-webkit-scrollbar-thumb:hover {
background: #a0aec0;
}
[data-theme="dark"] .search-results::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
.search-result-item {
background: rgba(248, 250, 252, 0.8);
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.8rem 1rem;
margin-bottom: 0.5rem;
transition: all 0.3s ease;
}
[data-theme="dark"] .search-result-item {
background: rgba(31, 41, 55, 0.8);
border: 1px solid #4b5563;
}
.search-result-item:hover {
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
[data-theme="dark"] .search-result-item:hover {
background: rgba(55, 65, 81, 0.95);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.result-model-name {
font-weight: 600;
font-size: 1rem;
color: #2d3748;
margin-bottom: 0.3rem;
transition: color 0.3s ease;
}
[data-theme="dark"] .result-model-name {
color: #f9fafb;
}
.result-meta {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: #666;
margin-bottom: 0.3rem;
flex-wrap: wrap;
transition: color 0.3s ease;
}
[data-theme="dark"] .result-meta {
color: #9ca3af;
}
.result-file-path {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.75rem;
color: #4a5568;
background: rgba(0, 0, 0, 0.05);
padding: 0.3rem 0.5rem;
border-radius: 4px;
margin-top: 0.3rem;
word-break: break-all;
transition: all 0.3s ease;
}
[data-theme="dark"] .result-file-path {
color: #d1d5db;
background: rgba(0, 0, 0, 0.3);
}
.search-info {
text-align: center;
padding: 2rem;
color: #666;
font-size: 0.9rem;
transition: color 0.3s ease;
}
[data-theme="dark"] .search-info {
color: #9ca3af;
}
.result-badge {
display: inline-block;
padding: 0.2rem 0.5rem;
background: #d97706;
color: white;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div class="header-text">
<h1>🤗 Transformers Models Timeline</h1>
<p>Interactive timeline to explore models supported by the Hugging Face Transformers library!</p>
</div>
<button id="themeToggle" class="theme-toggle" title="Toggle dark/light mode">
<span class="theme-icon">🌙</span>
</button>
</div>
</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>
<!-- Search Section -->
<div class="search-wrapper">
<div class="search-container">
<div class="search-header">
<h3>🔍 Search Modeling Files</h3>
<p>Search for strings in model implementation files (e.g., class names, function names)</p>
</div>
<div class="search-controls">
<input type="text" id="searchInput" placeholder="e.g., BaseModelOutputWithPooling" />
<button class="btn btn-primary" onclick="searchModelingFiles()">Search</button>
<button class="btn btn-secondary" onclick="clearSearchResults()">Clear</button>
</div>
<div id="searchResults" class="search-results">
<!-- Search results will be populated here -->
</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%)
let taskData = {}; // Store task data for easy lookup
// Theme management
let currentTheme = 'light';
// Theme detection and switching functions
function detectSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function loadTheme() {
// Check localStorage first, then system preference
const savedTheme = localStorage.getItem('theme');
const systemTheme = detectSystemTheme();
currentTheme = savedTheme || systemTheme;
applyTheme(currentTheme);
}
function applyTheme(theme) {
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
// Update theme toggle icon
const themeIcon = document.querySelector('.theme-icon');
if (themeIcon) {
themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙';
}
// Save to localStorage
localStorage.setItem('theme', theme);
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
}
// Listen for system theme changes
function setupThemeListener() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
// Only auto-switch if user hasn't manually set a preference
if (!localStorage.getItem('theme')) {
const newTheme = e.matches ? 'dark' : 'light';
applyTheme(newTheme);
}
});
}
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.style.left = '';
otherLabel.style.right = '';
otherLabel.style.transform = 'translateX(-50%)';
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);
const taskName = getTaskDisplayName(task);
return `<span class="task-badge" style="background-color: ${taskColor}">${taskName}</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';
// Calculate timeline container bounds
const timelineContainer = document.querySelector('.timeline-container');
const containerBounds = timelineContainer.getBoundingClientRect();
const timelineItemRect = item.getBoundingClientRect();
// Get expanded card dimensions (it's now rendered with expanded content)
const expandedRect = label.getBoundingClientRect();
const cardWidth = 550; // Known width from CSS
const cardHalfWidth = cardWidth / 2;
// Calculate timeline item's center position relative to container
const itemCenterX = timelineItemRect.left + (timelineItemRect.width / 2) - containerBounds.left;
// Calculate default centered position bounds
const defaultLeft = itemCenterX - cardHalfWidth;
const defaultRight = itemCenterX + cardHalfWidth;
// Container padding to ensure cards don't touch edges
const padding = 20;
const containerWidth = containerBounds.width;
// Determine optimal positioning
let finalTransform = 'translateX(-50%)'; // Default centered
let finalLeft = '';
let finalRight = '';
if (defaultLeft < padding) {
// Card would extend beyond left edge - align to left with padding
finalTransform = 'translateX(-5%)';
finalLeft = '';
} else if (defaultRight > (containerWidth - padding)) {
// Card would extend beyond right edge - align to right with padding
finalTransform = 'translateX(-95%)';
finalRight = '';
}
// Apply positioning adjustments
label.style.transform = finalTransform;
// if (finalLeft) label.style.left = finalLeft;
// if (finalRight) label.style.right = finalRight;
// Set vertical positioning to keep the correct edge fixed
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 = '';
label.style.left = '';
label.style.right = '';
// Reset transform: depends on current position
label.style.transform = 'translateX(-50%)';
item.style.zIndex = '';
}, 50);
}, 50);
}
});
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 = '';
// Store task data for easy lookup
data.tasks.forEach(task => {
taskData[task.key] = { name: task.name, color: task.color };
});
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
}
// Function to get task display name from loaded task data
function getTaskDisplayName(taskKey) {
// Use stored task data if available
if (taskData[taskKey]) {
return taskData[taskKey].name;
}
// Fallback to formatted key if not found
return taskKey.replace(/-/g, ' ').replace(/\b\\w/g, l => l.toUpperCase());
}
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 () => {
// Initialize theme
loadTheme();
setupThemeListener();
// Add theme toggle event listener
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
}
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.style.transform = 'translateX(-50%)';
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;
// Ensure panel starts collapsed by default
panel.classList.add('collapsed');
panel.style.maxHeight = '74px';
btn.textContent = '▸';
// Restore persisted state after a short delay to ensure DOM is ready
setTimeout(() => {
const saved = localStorage.getItem('filtersCollapsed');
if (saved === 'false') {
// Expand to full content height
panel.classList.remove('collapsed');
panel.style.maxHeight = panel.scrollHeight + 'px';
btn.textContent = '▾';
}
// If saved is 'true' or null, keep collapsed (default state)
}, 50);
}
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');
}
}
// Search functionality
async function searchModelingFiles() {
const searchInput = document.getElementById('searchInput');
const searchResults = document.getElementById('searchResults');
const query = searchInput.value.trim();
if (!query) {
searchResults.innerHTML = '<div class="search-info">Please enter a search query</div>';
return;
}
// Show loading state
searchResults.innerHTML = '<div class="search-info">Searching modeling files...</div>';
try {
const response = await fetch(`/api/search-files?query=${encodeURIComponent(query)}`);
const data = await response.json();
if (!data.success) {
searchResults.innerHTML = `<div class="search-info" style="color: #e53e3e;">Error: ${data.error}</div>`;
return;
}
if (data.results.length === 0) {
searchResults.innerHTML = `<div class="search-info">No modeling files found containing "${query}"</div>`;
return;
}
// Display results
let html = `<div class="search-info" style="margin-bottom: 1rem; font-weight: 600; color: #d97706;">
Found ${data.total_matches} match${data.total_matches !== 1 ? 'es' : ''} for "${query}"
${data.total_matches > 10 ? ' (showing top 10 most recent)' : ''}
</div>`;
data.results.forEach((result, index) => {
html += `
<div class="search-result-item">
<div class="result-model-name">
${index + 1}. ${result.model_name}
<span class="result-badge">${result.occurrences} occurrence${result.occurrences !== 1 ? 's' : ''}</span>
</div>
<div class="result-meta">
<span>📅 Added: ${result.date}</span>
<span>📄 Size: ${result.size_kb} KB</span>
</div>
<div class="result-file-path">${result.file_path}</div>
</div>
`;
});
searchResults.innerHTML = html;
} catch (error) {
searchResults.innerHTML = `<div class="search-info" style="color: #e53e3e;">Error: ${error.message}</div>`;
}
}
function clearSearchResults() {
document.getElementById('searchInput').value = '';
document.getElementById('searchResults').innerHTML = '';
}
// Allow Enter key to trigger search
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchModelingFiles();
}
});
}
});
</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()