Commit 084937e6 authored by fisherdaddy's avatar fisherdaddy

feat: Add AI Timeline page and localization support in multiple languages

parent a329b0f9
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Vertical timeline bar -->
<rect x="4" y="3" width="3" height="18" rx="1.5" fill="#6366F1"/>
<!-- Timeline events -->
<circle cx="16" cy="6" r="3" fill="#818CF8"/>
<circle cx="16" cy="12" r="3" fill="#4F46E5"/>
<circle cx="16" cy="18" r="3" fill="#6366F1"/>
<!-- Connecting lines -->
<path d="M7 6H13" stroke="#818CF8" stroke-width="2" stroke-linecap="round"/>
<path d="M7 12H13" stroke="#4F46E5" stroke-width="2" stroke-linecap="round"/>
<path d="M7 18H13" stroke="#6366F1" stroke-width="2" stroke-linecap="round"/>
<!-- AI circuit elements inside the nodes -->
<path d="M15 6H17" stroke="white" stroke-width="1" stroke-linecap="round"/>
<path d="M16 5V7" stroke="white" stroke-width="1" stroke-linecap="round"/>
<path d="M15 12L17 12" stroke="white" stroke-width="1" stroke-linecap="round"/>
<path d="M15.5 11L16.5 13" stroke="white" stroke-width="1" stroke-linecap="round"/>
<path d="M16.5 11L15.5 13" stroke="white" stroke-width="1" stroke-linecap="round"/>
<path d="M14.5 17.5L17.5 18.5" stroke="white" stroke-width="1" stroke-linecap="round"/>
<path d="M14.5 18.5L17.5 17.5" stroke="white" stroke-width="1" stroke-linecap="round"/>
</svg>
\ No newline at end of file
...@@ -32,6 +32,7 @@ const DrugsList = lazy(() => import('./components/DrugsList')); ...@@ -32,6 +32,7 @@ const DrugsList = lazy(() => import('./components/DrugsList'));
const DeepSeekTimeline = lazy(() => import('./components/DeepSeekTimeline')); const DeepSeekTimeline = lazy(() => import('./components/DeepSeekTimeline'));
const WechatFormatter = lazy(() => import('./components/WechatFormatter')); const WechatFormatter = lazy(() => import('./components/WechatFormatter'));
const ImageAnnotator = lazy(() => import('./components/ImageAnnotator')); const ImageAnnotator = lazy(() => import('./components/ImageAnnotator'));
const AITimelinePage = lazy(() => import('./pages/AITimelinePage'));
function App() { function App() {
return ( return (
...@@ -71,6 +72,7 @@ function App() { ...@@ -71,6 +72,7 @@ function App() {
<Route path="/deepseek-timeline" element={<DeepSeekTimeline />} /> <Route path="/deepseek-timeline" element={<DeepSeekTimeline />} />
<Route path="/wechat-formatter" element={<WechatFormatter />} /> <Route path="/wechat-formatter" element={<WechatFormatter />} />
<Route path="/image-annotator" element={<ImageAnnotator />} /> <Route path="/image-annotator" element={<ImageAnnotator />} />
<Route path="/ai-timeline" element={<AITimelinePage />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</Suspense> </Suspense>
......
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';
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);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
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([]);
// 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();
};
// Calculate the last updated date from the sorted events
const lastUpdatedDate = groupedEventsByDate.length > 0
? formatLastUpdatedDate(groupedEventsByDate[0].date)
: null;
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>
</div>
</>
);
};
export default AITimeline;
\ No newline at end of file
This diff is collapsed.
...@@ -263,5 +263,20 @@ ...@@ -263,5 +263,20 @@
"downloadButton": "Download", "downloadButton": "Download",
"noImageMessage": "Upload an image or provide an image URL to begin", "noImageMessage": "Upload an image or provide an image URL to begin",
"resetView": "Reset View" "resetView": "Reset View"
},
"aiTimeline": {
"title": "AI Important Events Timeline",
"description": "Display important events and model release timelines in the AI field",
"attribution": "Some data sources",
"lastUpdated": "Last Updated",
"categories": {
"all": "All Events",
"MODEL_RELEASE": "Model Release",
"RESEARCH": "Research",
"POLICY": "Policy",
"BUSINESS": "Business",
"CULTURE": "Culture",
"OPEN_SOURCE": "Open Source"
}
} }
} }
...@@ -260,5 +260,20 @@ ...@@ -260,5 +260,20 @@
"downloadButton": "ダウンロード", "downloadButton": "ダウンロード",
"noImageMessage": "画像をアップロードするか、画像URLを提供して開始してください", "noImageMessage": "画像をアップロードするか、画像URLを提供して開始してください",
"resetView": "ビューをリセット" "resetView": "ビューをリセット"
},
"aiTimeline": {
"title": "AI 重要事件時間軸",
"description": "AI 分野の重要な事件とモデルリリース時間軸を表示します",
"attribution": "一部のデータ出典",
"lastUpdated": "最終更新日",
"categories": {
"all": "すべての事件",
"MODEL_RELEASE": "モデルリリース",
"RESEARCH": "研究と論文",
"POLICY": "政策と規制",
"BUSINESS": "ビジネスと産業",
"CULTURE": "文化",
"OPEN_SOURCE": "オープンソース"
}
} }
} }
...@@ -261,5 +261,20 @@ ...@@ -261,5 +261,20 @@
"downloadButton": "다운로드", "downloadButton": "다운로드",
"noImageMessage": "이미지를 업로드하거나 이미지 URL을 제공하세요", "noImageMessage": "이미지를 업로드하거나 이미지 URL을 제공하세요",
"resetView": "뷰 초기화" "resetView": "뷰 초기화"
} },
"aiTimeline": {
"title": "AI 중요 사건 시간표",
"description": "AI 분야의 중요한 사건과 모델 출시 시간표를 표시합니다",
"attribution": "일부 데이터 출처",
"lastUpdated": "마지막 업데이트",
"categories": {
"all": "모든 사건",
"MODEL_RELEASE": "모델 출시",
"RESEARCH": "연구 및 논문",
"POLICY": "정치 및 규정",
"BUSINESS": "비즈니스 및 산업",
"CULTURE": "문화",
"OPEN_SOURCE": "오픈 소스"
}
}
} }
...@@ -244,7 +244,7 @@ ...@@ -244,7 +244,7 @@
}, },
"wechatFormatter": { "wechatFormatter": {
"title": "微信公众号排版助手", "title": "微信公众号排版助手",
"description": "Markdown、 HTML 格式内容一键即可转为微信公众号排版", "description": "Markdown、HTML 内容一键转为公众号版式",
"input": "输入内容", "input": "输入内容",
"output": "输出内容", "output": "输出内容",
"inputPlaceholder": "在此输入需要微信排版的文本", "inputPlaceholder": "在此输入需要微信排版的文本",
...@@ -265,5 +265,20 @@ ...@@ -265,5 +265,20 @@
"downloadButton": "下载", "downloadButton": "下载",
"noImageMessage": "上传图片或提供图片URL开始", "noImageMessage": "上传图片或提供图片URL开始",
"resetView": "重置视图" "resetView": "重置视图"
},
"aiTimeline": {
"title": "AI 重大事件一览",
"description": "展示 AI 领域的重大事件和模型发布时间线",
"attribution": "部分数据来源",
"lastUpdated": "最后更新时间",
"categories": {
"all": "全部事件",
"MODEL_RELEASE": "模型发布",
"RESEARCH": "研究与论文",
"POLICY": "政策与监管",
"BUSINESS": "商业与行业",
"CULTURE": "文化",
"OPEN_SOURCE": "开源"
}
} }
} }
import React from 'react';
import AITimeline from '../components/AITimeline';
const AITimelinePage = () => {
return <AITimeline />;
};
export default AITimelinePage;
\ No newline at end of file
...@@ -4,6 +4,7 @@ import { useTranslation } from '../js/i18n'; ...@@ -4,6 +4,7 @@ import { useTranslation } from '../js/i18n';
import SEO from '../components/SEO'; import SEO from '../components/SEO';
const tools = [ const tools = [
{ id: 'aiTimeline', icon: '/assets/icon/ai-timeline.svg', path: '/ai-timeline' },
{ id: 'openAITimeline', icon: '/assets/icon/openai_small.svg', path: '/openai-timeline' }, { id: 'openAITimeline', icon: '/assets/icon/openai_small.svg', path: '/openai-timeline' },
{ id: 'anthropicTimeline', icon: '/assets/icon/anthropic_small.svg', path: '/anthropic-timeline' }, { id: 'anthropicTimeline', icon: '/assets/icon/anthropic_small.svg', path: '/anthropic-timeline' },
{ id: 'deepSeekTimeline', icon: '/assets/icon/deepseek_small.jpg', path: '/deepseek-timeline' }, { id: 'deepSeekTimeline', icon: '/assets/icon/deepseek_small.jpg', path: '/deepseek-timeline' },
......
.vertical-timeline-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 6rem 1rem 2rem;
position: relative;
color: #1a1a1a;
}
.vertical-timeline-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
z-index: 1;
}
.timeline-title {
text-align: center;
font-size: 2.5rem;
margin-bottom: 3rem;
font-weight: 700;
letter-spacing: -0.02em;
position: relative;
z-index: 2;
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.category-filters {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 3rem;
flex-wrap: wrap;
position: relative;
z-index: 2;
}
.category-filter {
background: rgba(99, 102, 241, 0.1);
backdrop-filter: blur(5px);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 30px;
padding: 0.5rem 1.2rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
color: #4F46E5;
}
.category-filter.active {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
border-color: transparent;
color: white;
}
.category-filter:hover:not(.active) {
background: rgba(99, 102, 241, 0.2);
}
.timeline-events-list {
max-width: 800px;
margin: 0 auto;
position: relative;
z-index: 2;
padding: 0 1rem;
}
.timeline-events-list::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: calc(50px + 1rem);
width: 3px;
background: linear-gradient(to bottom, rgba(99, 102, 241, 0.1), rgba(79, 70, 229, 0.3), rgba(99, 102, 241, 0.1));
border-radius: 1.5px;
transform: translateX(-50%);
}
.timeline-year-separator {
text-align: center;
font-size: 1.6rem;
margin: 2.5rem 0 1.5rem;
color: #4F46E5;
position: relative;
z-index: 3;
background: #f5f7ff;
display: inline-block;
padding: 0 1rem;
left: 50%;
transform: translateX(-50%);
}
.timeline-event-item {
display: flex;
align-items: flex-start;
gap: 1.5rem;
position: relative;
margin-bottom: 2rem;
padding-left: calc(50px + 1rem + 20px);
}
.event-date {
position: absolute;
left: 0;
top: 5px;
width: 50px;
text-align: right;
font-size: 0.85rem;
color: #6b7280;
font-weight: 500;
white-space: nowrap;
}
.timeline-event-item::before {
content: '';
position: absolute;
left: calc(50px + 1rem);
top: 12px;
width: 13px;
height: 13px;
border-radius: 50%;
background: white;
border: 3px solid #6366F1;
transform: translateX(-50%);
z-index: 3;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.event-cards-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
flex-grow: 1;
}
.event-link {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border-radius: 10px;
border: 1px solid rgba(99, 102, 241, 0.15);
padding: 1rem 1.2rem;
transition: all 0.3s ease, max-height 0.4s ease-in-out;
cursor: pointer;
box-shadow: 0 6px 25px rgba(99, 102, 241, 0.08);
text-decoration: none;
color: #1a1a1a;
display: block;
position: relative;
overflow: hidden;
}
.event-link:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.3);
}
.event-content {
/* Container inside link if needed, or apply styles directly to .event-link */
}
.event-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #374151;
}
.event-description {
font-size: 0.95rem;
line-height: 1.5;
color: #4b5563;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
max-height: calc(1.5em);
transition: max-height 0.3s ease-in-out;
}
.event-link:hover .event-description {
-webkit-line-clamp: unset;
max-height: 100px;
}
.event-arrow {
position: absolute;
top: 0.8rem;
right: 0.8rem;
font-size: 1.1rem;
color: rgba(99, 102, 241, 0.5);
transition: all 0.3s ease;
opacity: 0;
}
.event-link:hover .event-arrow {
opacity: 1;
color: #4F46E5;
transform: translate(2px, -2px);
}
.timeline-event-item.model-release::before {
border-color: #6366F1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.timeline-event-item.open-source::before {
border-color: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.1);
}
.timeline-event-item.business-industry::before {
border-color: #F59E0B;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
}
.timeline-event-item.research-papers::before {
border-color: #10B981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
.timeline-event-item.policy-regulation::before {
border-color: #EF4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
.timeline-event-item.culture::before {
border-color: #EC4899;
box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.1);
}
@media (max-width: 768px) {
.vertical-timeline-container {
padding: 5rem 0.5rem 1.5rem;
}
.timeline-title {
font-size: 2rem;
margin-bottom: 2rem;
}
.category-filters {
gap: 0.5rem;
margin-bottom: 2rem;
}
.category-filter {
padding: 0.4rem 1rem;
font-size: 0.85rem;
}
.timeline-events-list {
padding: 0 0.5rem;
}
.timeline-events-list::before {
left: calc(40px + 0.5rem);
}
.timeline-event-item {
padding-left: calc(40px + 0.5rem + 15px);
gap: 1rem;
}
.event-date {
width: 40px;
font-size: 0.8rem;
}
.timeline-event-item::before {
left: calc(40px + 0.5rem);
top: 10px;
width: 11px;
height: 11px;
border-width: 2px;
}
.event-cards-container {
gap: 0.5rem;
}
.event-link {
padding: 0.8rem 1rem;
}
.event-title {
font-size: 1rem;
}
.event-description {
font-size: 0.9rem;
max-height: calc(1.5em);
}
.event-link:hover .event-description {
max-height: 80px;
}
.timeline-year-separator {
font-size: 1.5rem;
}
}
/* Styles for Attribution and Last Updated */
.timeline-meta-info {
position: absolute;
top: 6rem;
left: 1rem;
z-index: 2;
text-align: left;
}
.timeline-meta-info p {
font-size: 0.8rem;
color: #6b7280;
margin-bottom: 0.3rem;
line-height: 1.3;
}
.timeline-meta-info a {
color: #4F46E5;
text-decoration: none;
transition: color 0.2s ease;
}
.timeline-meta-info a:hover {
color: #3730a3;
text-decoration: underline;
}
/* Responsive adjustments for meta info */
@media (max-width: 768px) {
.timeline-meta-info {
top: 5rem;
left: 0.5rem;
}
.timeline-meta-info p {
font-size: 0.75rem;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment