|
|
|
|
|
""" |
|
|
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 |
|
|
except Exception: |
|
|
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 = {} |
|
|
|
|
|
|
|
|
transformers_src = os.path.join(os.path.dirname(docs_dir), "..", "..", "src") |
|
|
if transformers_src not in sys.path: |
|
|
sys.path.insert(0, transformers_src) |
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
nested = sec.get("sections") or [] |
|
|
for sub in nested: |
|
|
if not isinstance(sub, dict): |
|
|
continue |
|
|
|
|
|
if "local" in sub: |
|
|
local = sub.get("local") |
|
|
if isinstance(local, str) and local.startswith("model_doc/"): |
|
|
result.append(local.split("/", 1)[1]) |
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
if not any([text_models, vision_models, audio_models, video_models, multimodal_models]): |
|
|
return None |
|
|
|
|
|
|
|
|
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"]} |
|
|
|
|
|
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...") |
|
|
|
|
|
subprocess.run(["git", "clone", "https://github.com/huggingface/transformers.git", repo_dir], check=True) |
|
|
subprocess.run(["pip", "install", "-e", repo_dir], check=True) |
|
|
else: |
|
|
|
|
|
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") |
|
|
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 |
|
|
|
|
|
|
|
|
@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: |
|
|
|
|
|
models = parser.parse_all_model_dates() |
|
|
model_to_date = {m["model_name"]: m["transformers_date"] for m in models} |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
matching_files = [] |
|
|
|
|
|
for model_name, date_str in model_to_date.items(): |
|
|
|
|
|
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 = content.count(search_string) |
|
|
|
|
|
file_size = os.path.getsize(modeling_file) / 1024 |
|
|
|
|
|
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 |
|
|
except Exception as e: |
|
|
print(f"Error reading {modeling_file}: {e}") |
|
|
continue |
|
|
|
|
|
|
|
|
matching_files.sort(key=lambda x: x["date"], reverse=True) |
|
|
|
|
|
|
|
|
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_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() |
|
|
|