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