AITimeline.jsx 6.89 KB
Newer Older
1 2 3 4 5 6 7 8
import React, { useState, useEffect } from 'react';
import { useScrollToTop } from '../hooks/useScrollToTop';
import '../styles/HorizontalTimeline.css';
import events from '../data/ai-events.json';
import SEO from './SEO';
import { useTranslation } from '../js/i18n';
import { usePageLoading } from '../hooks/usePageLoading';
import LoadingOverlay from './LoadingOverlay';
9
import ScrollToTopButton from './ScrollToTopButton';
10 11 12 13 14 15 16 17 18 19 20 21 22

const categories = [
  { id: 'all', label: 'All Events' },
  { id: 'MODEL_RELEASE', label: 'Model Release' },
  { id: 'RESEARCH', label: 'Research & Papers' },
  { id: 'POLICY', label: 'Policy & Regulation' },
  { id: 'BUSINESS', label: 'Business & Industry' },
  { id: 'CULTURE', label: 'Culture' },
  { id: 'OPEN_SOURCE', label: 'Open Source' }
];

const formatDate = (dateString) => {
  const date = new Date(dateString);
23 24 25
  const month = date.getMonth() + 1; // Months are 0-based
  const day = date.getDate();
  return `${month}.${day}`;
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
};

const getCategoryClass = (categoryArray) => {
  const firstCategory = categoryArray && categoryArray.length > 0 ? categoryArray[0] : null;
  const classes = {
    'MODEL_RELEASE': 'model-release',
    'BUSINESS': 'business-industry',
    'RESEARCH': 'research-papers',
    'POLICY': 'policy-regulation',
    'CULTURE': 'culture',
    'OPEN_SOURCE': 'open-source'
  };
  return firstCategory ? classes[firstCategory] || '' : '';
};

// Helper function to format the last updated date
const formatLastUpdatedDate = (dateString) => {
  const date = new Date(dateString);
  // Using Chinese locale for format YYYY年M月D日
  return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
};

const AITimeline = () => {
  useScrollToTop();
  const { t } = useTranslation();
  const isLoading = usePageLoading();
  const [selectedCategory, setSelectedCategory] = useState('all');
  const [groupedEventsByDate, setGroupedEventsByDate] = useState([]);
54 55 56 57 58 59 60 61 62 63
  const [lastUpdatedDate, setLastUpdatedDate] = useState(null);

  // Set the last updated date once on component mount
  useEffect(() => {
    // Sort events by date descending to get the most recent event
    const sortedEvents = [...events].sort((a, b) => new Date(b.date) - new Date(a.date));
    if (sortedEvents.length > 0) {
      setLastUpdatedDate(formatLastUpdatedDate(sortedEvents[0].date));
    }
  }, []);
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174

  // Group events by date after sorting descendingly
  useEffect(() => {
    const filtered = selectedCategory === 'all'
      ? events
      : events.filter(event => event.category.includes(selectedCategory));

    // Sort events by date descending
    const sorted = filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
    
    // Group sorted events by date
    const grouped = sorted.reduce((acc, event) => {
      const date = event.date;
      if (!acc[date]) {
        acc[date] = [];
      }
      acc[date].push(event);
      return acc;
    }, {});

    // Convert grouped object to array of { date, events } sorted by date descending
    const groupedArray = Object.keys(grouped)
      .sort((a, b) => new Date(b) - new Date(a)) // Ensure dates are sorted descendingly
      .map(date => ({ date, events: grouped[date] }));

    setGroupedEventsByDate(groupedArray);
  }, [selectedCategory]);

  const handleCategoryClick = (categoryId) => {
    setSelectedCategory(categoryId);
  };

  // Helper to check if year changes for year separators
  const shouldShowYearSeparator = (currentDateGroup, previousDateGroup) => {
    if (!previousDateGroup) {
      return true; // Show year for the first group
    }
    return new Date(currentDateGroup.date).getFullYear() !== new Date(previousDateGroup.date).getFullYear();
  };

  return (
    <>
      <SEO
        title={t('tools.aiTimeline.title', 'AI Major Events Timeline')}
        description={t('tools.aiTimeline.description', 'A timeline of major events in AI development, research, and regulation')}
      />
      {isLoading && <LoadingOverlay />}
      <div className="vertical-timeline-container">
        <h1 className="timeline-title">{t('tools.aiTimeline.title', 'AI Major Events Timeline')}</h1>
        
        <div className="category-filters">
          {categories.map(category => (
            <button
              key={category.id}
              className={`category-filter ${selectedCategory === category.id ? 'active' : ''}`}
              onClick={() => handleCategoryClick(category.id)}
            >
              {t(`tools.aiTimeline.categories.${category.id}`, category.label)}
            </button>
          ))}
        </div>
        
        {/* Attribution and Last Updated Section */}
        <div className="timeline-meta-info">
          <p className="timeline-attribution">
            {t('tools.aiTimeline.attribution', '部分数据参考自')} <a href="https://ai-timeline.org/" target="_blank" rel="noopener noreferrer">ai-timeline.org</a>
          </p>
          {lastUpdatedDate && (
            <p className="timeline-last-updated">
              {t('tools.aiTimeline.lastUpdated', '最近更新')}: {lastUpdatedDate}
            </p>
          )}
        </div>

        <div className="timeline-events-list">
          {groupedEventsByDate.map((dateGroup, index) => {
            const currentYear = new Date(dateGroup.date).getFullYear();
            const showYearSeparator = shouldShowYearSeparator(dateGroup, groupedEventsByDate[index - 1]);
            const markerCategoryClass = dateGroup.events.length > 0 ? getCategoryClass(dateGroup.events[0].category) : '';

            return (
              <React.Fragment key={dateGroup.date}>
                {showYearSeparator && (
                  <div className="timeline-year-separator">
                    {currentYear}
                  </div>
                )}
                <div className={`timeline-event-item ${markerCategoryClass}`}>
                  <div className="event-date">{formatDate(dateGroup.date)}</div>
                  <div className="event-cards-container">
                    {dateGroup.events.map((event, eventIndex) => (
                      <a
                        href={event.link}
                        target="_blank"
                        rel="noopener noreferrer"
                        className={`event-link ${getCategoryClass(event.category)}`}
                        key={event.id || `${dateGroup.date}-${eventIndex}`}
                      >
                        <div className="event-content">
                          <div className="event-title">{event.title}</div>
                          <div className="event-description">{event.description}</div>
                          <div className="event-arrow"></div>
                        </div>
                      </a>
                    ))}
                  </div>
                </div>
              </React.Fragment>
            );
          })}
        </div>
175 176 177
        
        {/* Scroll to top button */}
        <ScrollToTopButton />
178 179 180 181 182 183
      </div>
    </>
  );
};

export default AITimeline;