Skip to content

Commit 5700deb

Browse files
committed
.
1 parent 6e675a5 commit 5700deb

3 files changed

Lines changed: 232 additions & 38 deletions

File tree

main.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
from tqdm import tqdm
77
from concurrent.futures import ThreadPoolExecutor, as_completed
88
import os
9+
import time
910

1011
load_dotenv() # 加载 .env 文件中的环境变量
1112

1213
# Construct a BigQuery client object.
13-
client = bigquery.Client(project='gen-lang-client-0208180925')
14+
client = bigquery.Client(project='gen-lang-client-0730994553')
1415

1516
# 获取今天的日期和前一天的日期
1617
today = datetime.today()
@@ -125,18 +126,88 @@ def fetch_repo_details(repo_name):
125126
# 处理并行化请求
126127
def fetch_repo_details_parallel(df):
127128
results = []
128-
129+
129130
# 使用 ThreadPoolExecutor 进行并行化
130131
with ThreadPoolExecutor(max_workers=10) as executor:
131132
futures = {executor.submit(fetch_repo_details, row["repo_name"]): index for index, row in df.iterrows()}
132-
133+
133134
# 显示进度条
134135
for future in tqdm(as_completed(futures), total=len(futures), desc="Fetching repo details"):
135136
index = futures[future]
136137
created_at, stargazer_count = future.result()
137138
df.at[index, "created_at"] = created_at
138139
df.at[index, "current_star_count"] = stargazer_count
139-
140+
141+
return df
142+
143+
# AI 总结函数(使用 OpenRouter)
144+
def summarize_with_openrouter(repo_name, star_count, created_at, current_stars):
145+
"""
146+
使用 OpenRouter API 生成项目总结
147+
"""
148+
from openai import OpenAI
149+
150+
api_key = os.environ.get("OPENROUTER_API_KEY", "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")
151+
client = OpenAI(
152+
base_url="https://openrouter.ai/api/v1",
153+
api_key=api_key,
154+
)
155+
156+
prompt = f"""请分析以下 GitHub 项目,用简洁的中文总结(100字以内):
157+
158+
项目名称:{repo_name}
159+
最近新增星标:{star_count}
160+
当前总星标:{current_stars}
161+
创建时间:{created_at}
162+
163+
请回答:
164+
1. 这个项目是做什么的?(推测)
165+
2. 为什么值得关注?
166+
167+
输出格式:直接输出总结内容,不要标题。"""
168+
169+
try:
170+
response = client.chat.completions.create(
171+
model="xiaomi/mimo-v2-flash:free",
172+
messages=[
173+
{"role": "system", "content": "你是一个技术分析师,擅长分析 GitHub 项目。请用简洁的中文回答。"},
174+
{"role": "user", "content": prompt}
175+
]
176+
)
177+
return response.choices[0].message.content.strip()
178+
except Exception as e:
179+
print(f"OpenRouter API exception for {repo_name}: {str(e)}")
180+
return None
181+
182+
# 批量生成总结(为前 N 个项目生成)
183+
def generate_summaries(df, top_n=50):
184+
"""
185+
为前 top_n 个项目生成 AI 总结(并发)
186+
"""
187+
df["ai_summary"] = None
188+
189+
def process_single_repo(index):
190+
row = df.iloc[index]
191+
repo_name = row["repo_name"]
192+
star_count = row["star_count"]
193+
created_at = row["created_at"]
194+
current_stars = row["current_star_count"]
195+
196+
# 跳过无效数据
197+
if pd.isna(created_at) or current_stars is None:
198+
return index, None
199+
200+
summary = summarize_with_openrouter(repo_name, star_count, created_at, current_stars)
201+
return index, summary
202+
203+
# 使用 ThreadPoolExecutor 并发处理
204+
with ThreadPoolExecutor(max_workers=10) as executor:
205+
futures = {executor.submit(process_single_repo, i): i for i in range(min(top_n, len(df)))}
206+
207+
for future in tqdm(as_completed(futures), total=len(futures), desc="Generating AI summaries"):
208+
index, summary = future.result()
209+
df.at[df.index[index], "ai_summary"] = summary
210+
140211
return df
141212

142213
# 假设你已经得到了如下的 DataFrame
@@ -149,6 +220,9 @@ def fetch_repo_details_parallel(df):
149220
# 执行并行化的获取仓库信息
150221
df = fetch_repo_details_parallel(df)
151222

223+
# 生成 AI 总结(为前 50 个项目)
224+
df = generate_summaries(df, top_n=50)
225+
152226
# 确保 created_at 是 datetime 类型
153227
df["created_at"] = pd.to_datetime(df["created_at"])
154228

web/app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
1313
});
1414

1515
export const metadata: Metadata = {
16-
title: "Create Next App",
17-
description: "Generated by create next app",
16+
title: "GitHub 热门项目预测器",
17+
description: "基于 AI 分析的 GitHub 潜力项目发现工具",
1818
};
1919

2020
export default function RootLayout({

web/app/page.tsx

Lines changed: 152 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,185 @@
22

33
import { useEffect, useState } from 'react';
44

5+
interface RepoData {
6+
name: string;
7+
stars: string;
8+
createdAt: string;
9+
currentStars: string;
10+
summary?: string;
11+
}
12+
513
export default function Home() {
6-
const [data, setData] = useState<string[][]>([]);
14+
const [data, setData] = useState<RepoData[]>([]);
715
const [error, setError] = useState<string | null>(null);
16+
const [loading, setLoading] = useState(false);
17+
const [expandedSummaries, setExpandedSummaries] = useState<Set<string>>(new Set());
818

919
useEffect(() => {
1020
const fetchData = async () => {
21+
setLoading(true);
1122
try {
1223
const response = await fetch('/results/result.csv');
1324
if (!response.ok) {
1425
throw new Error(`HTTP error! status: ${response.status}`);
1526
}
1627
const text = await response.text();
17-
// Simple CSV parsing
18-
const rows = text.split('\n').map(row => row.split(','));
19-
setData(rows);
28+
// Parse CSV handling quoted fields
29+
const parseCSV = (text: string) => {
30+
const rows: string[][] = [];
31+
let currentRow: string[] = [];
32+
let currentField = '';
33+
let inQuotes = false;
34+
35+
for (let i = 0; i < text.length; i++) {
36+
const char = text[i];
37+
const nextChar = text[i + 1];
38+
39+
if (char === '"') {
40+
if (inQuotes && nextChar === '"') {
41+
currentField += '"';
42+
i++;
43+
} else {
44+
inQuotes = !inQuotes;
45+
}
46+
} else if (char === ',' && !inQuotes) {
47+
currentRow.push(currentField);
48+
currentField = '';
49+
} else if ((char === '\n' || char === '\r') && !inQuotes) {
50+
if (currentField || currentRow.length > 0) {
51+
currentRow.push(currentField);
52+
rows.push(currentRow);
53+
currentRow = [];
54+
currentField = '';
55+
}
56+
if (char === '\r' && nextChar === '\n') i++;
57+
} else {
58+
currentField += char;
59+
}
60+
}
61+
62+
if (currentField || currentRow.length > 0) {
63+
currentRow.push(currentField);
64+
rows.push(currentRow);
65+
}
66+
67+
return rows;
68+
};
69+
70+
const rows = parseCSV(text).filter(row => row.some(cell => cell.trim()));
71+
const repoData: RepoData[] = rows.slice(1).map(row => ({
72+
name: row[0] || '',
73+
stars: row[1] || '0',
74+
createdAt: row[2] || '',
75+
currentStars: row[3] || '0',
76+
summary: row[4] || '',
77+
})).filter(r => r.name);
78+
setData(repoData);
2079
} catch (e: any) {
2180
setError(e.message);
81+
} finally {
82+
setLoading(false);
2283
}
2384
};
2485

2586
fetchData();
2687
}, []);
2788

89+
const toggleSummary = (repoName: string) => {
90+
setExpandedSummaries(prev => {
91+
const newSet = new Set(prev);
92+
if (newSet.has(repoName)) {
93+
newSet.delete(repoName);
94+
} else {
95+
newSet.add(repoName);
96+
}
97+
return newSet;
98+
});
99+
};
100+
28101
if (error) {
29-
return <div className="p-8">Error: {error}</div>;
102+
return <div className="p-8 text-red-500">Error: {error}</div>;
30103
}
31104

32-
if (data.length === 0) {
105+
if (loading) {
33106
return <div className="p-8">Loading data...</div>;
34107
}
35108

36109
return (
37-
<div className="p-8">
38-
<h1 className="text-2xl font-bold mb-4">GitHub Analysis Results</h1>
39-
<div className="overflow-x-auto">
40-
<table className="min-w-full bg-white border border-gray-300">
41-
<thead>
42-
{data[0] && (
43-
<tr>
44-
{data[0].map((header, index) => (
45-
<th key={index} className="py-2 px-4 border-b text-left">
46-
{header}
47-
</th>
48-
))}
49-
</tr>
50-
)}
51-
</thead>
52-
<tbody>
53-
{data.slice(1).map((row, rowIndex) => (
54-
<tr key={rowIndex} className="hover:bg-gray-50">
55-
{row.map((cell, cellIndex) => (
56-
<td key={cellIndex} className="py-2 px-4 border-b">
57-
{cell}
58-
</td>
110+
<div className="min-h-screen bg-gray-50 p-4 md:p-8">
111+
<div className="max-w-7xl mx-auto">
112+
<h1 className="text-2xl md:text-3xl font-bold mb-2 text-gray-900">GitHub 热门项目预测器</h1>
113+
<p className="text-gray-600 mb-6">基于最近 2 天 star 增长分析的潜力项目,AI 智能总结</p>
114+
115+
<div className="bg-white rounded-lg shadow overflow-hidden">
116+
<div className="overflow-x-auto">
117+
<table className="min-w-full">
118+
<thead className="bg-gray-100">
119+
<tr>
120+
<th className="py-3 px-2 md:px-4 text-left text-xs md:text-sm font-semibold text-gray-700">排名</th>
121+
<th className="py-3 px-2 md:px-4 text-left text-xs md:text-sm font-semibold text-gray-700">项目名称</th>
122+
<th className="py-3 px-2 md:px-4 text-left text-xs md:text-sm font-semibold text-gray-700">新增</th>
123+
<th className="py-3 px-2 md:px-4 text-left text-xs md:text-sm font-semibold text-gray-700">总 Star</th>
124+
<th className="py-3 px-2 md:px-4 text-left text-xs md:text-sm font-semibold text-gray-700 hidden md:table-cell">创建时间</th>
125+
<th className="py-3 px-2 md:px-4 text-left text-xs md:text-sm font-semibold text-gray-700">AI 总结</th>
126+
</tr>
127+
</thead>
128+
<tbody className="divide-y divide-gray-200">
129+
{data.map((repo, index) => (
130+
<>
131+
<tr key={repo.name} className="hover:bg-gray-50">
132+
<td className="py-3 px-2 md:px-4 text-sm text-gray-900">{index + 1}</td>
133+
<td className="py-3 px-2 md:px-4">
134+
<a
135+
href={`https://github.com/${repo.name}`}
136+
target="_blank"
137+
rel="noopener noreferrer"
138+
className="text-blue-600 hover:text-blue-800 font-medium text-xs md:text-sm"
139+
>
140+
{repo.name}
141+
</a>
142+
</td>
143+
<td className="py-3 px-2 md:px-4 text-sm text-gray-900">
144+
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
145+
+{repo.stars}
146+
</span>
147+
</td>
148+
<td className="py-3 px-2 md:px-4 text-sm text-gray-600">{repo.currentStars}</td>
149+
<td className="py-3 px-2 md:px-4 text-sm text-gray-600 hidden md:table-cell">
150+
{new Date(repo.createdAt).toLocaleDateString('zh-CN')}
151+
</td>
152+
<td className="py-3 px-2 md:px-4">
153+
{repo.summary ? (
154+
<button
155+
onClick={() => toggleSummary(repo.name)}
156+
className="text-xs px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 transition-colors"
157+
>
158+
{expandedSummaries.has(repo.name) ? '收起' : '查看'}
159+
</button>
160+
) : (
161+
<span className="text-xs text-gray-400">-</span>
162+
)}
163+
</td>
164+
</tr>
165+
{repo.summary && expandedSummaries.has(repo.name) && (
166+
<tr key={`${repo.name}-summary`}>
167+
<td colSpan={6} className="py-4 px-2 md:px-4 bg-blue-50">
168+
<div className="text-sm text-gray-700 leading-relaxed whitespace-pre-wrap">
169+
{repo.summary}
170+
</div>
171+
</td>
172+
</tr>
173+
)}
174+
</>
59175
))}
60-
</tr>
61-
))}
62-
</tbody>
63-
</table>
176+
</tbody>
177+
</table>
178+
</div>
179+
</div>
180+
181+
<p className="mt-4 text-xs md:text-sm text-gray-500">
182+
数据更新时间: {new Date().toLocaleString('zh-CN')}
183+
</p>
64184
</div>
65185
</div>
66186
);

0 commit comments

Comments
 (0)