github-search-zh / index.html
Lowking's picture
feat: Update GA4 tracking (G-4M6KQ8KSY5) and analytics dashboard
2291b2d
Raw
History Blame Contribute Delete
42.3 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub 繁中快搜</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config={darkMode:"class"}</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-4M6KQ8KSY5"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-4M6KQ8KSY5');
</script>
<style>
body{font-family:'Inter',sans-serif;background:#020817;color:#f1f5f9;}
@keyframes shimmer{0%{background-position:-800px 0}100%{background-position:800px 0}}
.sk{animation:shimmer 1.5s infinite linear;background:linear-gradient(90deg,#1e293b 25%,#334155 50%,#1e293b 75%);background-size:800px 100%;border-radius:4px;}
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.fi{animation:fi .25s ease forwards;}
.card:hover{border-color:#6366f1;}
</style>
</head>
<body class="dark min-h-screen">
<!-- HEADER -->
<header class="sticky top-0 z-40 border-b border-slate-800 bg-slate-950/95 backdrop-blur">
<div class="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="bg-indigo-600 rounded-lg p-1.5">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65S9 17.23 9 18v4"/>
<path d="M9 18c-4.51 2-5-2-7-2"/>
</svg>
</div>
<div>
<h1 class="font-bold text-base flex items-center gap-2">
GitHub 繁中快搜
<span class="text-[10px] bg-indigo-500/20 text-indigo-400 border border-indigo-500/30 px-2 py-0.5 rounded-full">AI 版</span>
</h1>
<p class="text-[11px] text-slate-500 hidden sm:block">輸入中文需求 &rarr; 英文關鍵字 &rarr; GitHub 精準搜尋</p>
</div>
</div>
<div class="flex items-center gap-2">
<button id="btn-favs" onclick="toggleFavsView()" class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 text-amber-300 text-sm transition">
⭐ 我的收藏 (<span id="fav-count">0</span>)
</button>
<select id="text-size" onchange="changeTextSize(this.value)" class="bg-slate-800 border border-slate-700 text-slate-300 text-sm rounded-lg px-2 py-1.5 focus:outline-none focus:border-indigo-500">
<option value="100%">大小: 100% (小)</option>
<option value="150%">大小: 150% (中)</option>
<option value="200%">大小: 200% (大)</option>
</select>
<button
id="btn-settings"
onclick="openModal()"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 text-sm transition"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
設定
</button>
</div>
</div>
</header>
<!-- MAIN -->
<main class="max-w-6xl mx-auto px-4 py-8 flex flex-col gap-6">
<!-- Search Panel -->
<section class="bg-slate-900 rounded-2xl border border-slate-800 p-5">
<form id="search-form" onsubmit="handleSubmit(event)" class="flex flex-col gap-4">
<div class="relative flex items-center">
<svg class="w-4 h-4 absolute left-4 text-slate-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
<input
id="search-query"
type="text"
autocomplete="off"
placeholder="輸入需求,例如:自動整理信件的 Python 工具、好用的 CLI 終端工具..."
class="w-full pl-11 pr-28 py-3 bg-slate-950 border border-slate-700 rounded-xl focus:outline-none focus:border-indigo-500 text-slate-100 placeholder-slate-600 text-sm transition"
/>
<button
type="submit"
class="absolute right-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg text-sm transition"
>
智慧搜尋
</button>
</div>
<!-- History -->
<div id="history-bar" class="hidden flex-wrap items-center gap-2">
<span class="text-xs text-slate-600">最近:</span>
<div id="history-chips" class="flex flex-wrap gap-1.5"></div>
</div>
<!-- Filters -->
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
<select id="f-lang" class="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded-lg px-3 py-2 focus:outline-none focus:border-indigo-500">
<option value="">全部語言</option>
<option value="python">Python</option>
<option value="javascript">JavaScript</option>
<option value="typescript">TypeScript</option>
<option value="rust">Rust</option>
<option value="go">Go</option>
<option value="cpp">C++</option>
<option value="java">Java</option>
<option value="shell">Shell</option>
</select>
<select id="f-sort" class="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded-lg px-3 py-2 focus:outline-none focus:border-indigo-500">
<option value="stars">最多 Stars</option>
<option value="forks">最多 Forks</option>
<option value="updated">最近更新</option>
</select>
<select id="f-stars" class="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded-lg px-3 py-2 focus:outline-none focus:border-indigo-500">
<option value="">不限 Stars</option>
<option value="50">&gt; 50</option>
<option value="100">&gt; 100</option>
<option value="500">&gt; 500</option>
<option value="1000">&gt; 1,000</option>
<option value="5000">&gt; 5,000</option>
</select>
<select id="f-time" class="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded-lg px-3 py-2 focus:outline-none focus:border-indigo-500">
<option value="">不限時間</option>
<option value="30">最近 1 個月</option>
<option value="180">最近半年</option>
<option value="365">最近一年</option>
</select>
<select id="f-pp" class="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded-lg px-3 py-2 focus:outline-none focus:border-indigo-500">
<option value="12">12 筆/頁</option>
<option value="24">24 筆/頁</option>
<option value="30">30 筆/頁</option>
</select>
</div>
</form>
</section>
<!-- Status Bar -->
<div id="status-bar" style="display:none" class="bg-indigo-950/40 border border-indigo-800/40 rounded-xl p-4 flex flex-col gap-3">
<div class="flex items-center gap-3 text-indigo-300 text-sm">
<div id="spinner" style="display:none" class="w-4 h-4 border-2 border-indigo-400 border-t-transparent rounded-full animate-spin"></div>
<span id="status-text">處理中...</span>
</div>
<div id="query-row" style="display:none" class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<div class="flex-1">
<div class="text-[11px] text-indigo-500 mb-1">搜尋關鍵字(可修改後重搜):</div>
<input id="query-display" type="text" class="w-full bg-transparent text-indigo-200 text-xs font-mono px-2 py-1 rounded border border-indigo-800/50 focus:outline-none focus:border-indigo-500"/>
</div>
<button onclick="reSearch()" class="px-3 py-1.5 bg-indigo-800/40 hover:bg-indigo-700/50 text-indigo-300 border border-indigo-700/40 rounded-lg text-xs transition whitespace-nowrap">
以此語法重搜
</button>
</div>
</div>
<!-- Results -->
<section>
<div class="flex items-center justify-between mb-4">
<h2 class="font-semibold text-slate-300">
搜尋結果
<span id="result-count" style="display:none" class="text-xs text-slate-500 bg-slate-900 border border-slate-700 px-2 py-0.5 rounded ml-2 font-normal"></span>
</h2>
</div>
<div id="empty-state" class="border border-dashed border-slate-800 rounded-2xl p-16 text-center">
<svg class="w-10 h-10 mx-auto text-slate-700 mb-3" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6M8 11h6"/>
</svg>
<p id="empty-title" class="text-slate-500 font-medium">尚未搜尋</p>
<p id="empty-desc" class="text-slate-600 text-sm mt-1">在上方輸入中文或英文需求,即可搜尋 GitHub 開源工具</p>
</div>
<div id="results-grid" style="display:none" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-6"></div>
<div id="pagination" style="display:none" class="flex items-center justify-center gap-4 pt-4">
<button onclick="changePage(-1)" id="btn-prev" class="px-4 py-2 bg-slate-900 border border-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed transition">&larr; 上一頁</button>
<span id="page-info" class="text-sm text-slate-500"></span>
<button onclick="changePage(1)" id="btn-next" class="px-4 py-2 bg-slate-900 border border-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed transition">下一頁 &rarr;</button>
</div>
</section>
</main>
<!-- FOOTER -->
<footer class="border-t border-slate-800 bg-slate-950/50 mt-12 py-6 mt-auto">
<div class="max-w-6xl mx-auto px-4 text-center flex flex-col gap-1.5">
<p class="text-xs text-slate-500">
&copy; 2026 GitHub 繁中快搜 | 專為精準開源搜尋打造
</p>
<p class="text-[0.7rem] text-slate-600">
開發者:Lowking Nowbucyang
</p>
</div>
</footer>
<!-- Settings Modal -->
<div id="modal" style="display:none;position:fixed;inset:0;z-index:50;background:rgba(0,0,0,.7);align-items:center;justify-content:center;padding:1rem;">
<div class="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-md p-6 shadow-2xl flex flex-col gap-5">
<div class="flex items-center justify-between">
<h3 class="font-bold">API 設定</h3>
<button onclick="closeModal()" class="text-slate-500 hover:text-slate-300 text-2xl leading-none">&times;</button>
</div>
<div class="space-y-4">
<div>
<div class="flex justify-between mb-1">
<label class="text-xs text-slate-400 font-medium">Gemini API Key(建議填寫)</label>
<a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-xs text-indigo-400 hover:underline">取得 &rarr;</a>
</div>
<input id="input-gemini" type="password" placeholder="AIzaSyxxxxxxxxxxxx"
class="w-full px-3 py-2 bg-slate-950 border border-slate-700 rounded-lg text-sm text-slate-200 focus:outline-none focus:border-indigo-500"/>
<p class="text-[11px] text-slate-600 mt-1">填寫以啟用專案卡片上的 ✨ AI 專案分析報告功能</p>
</div>
<div>
<div class="flex justify-between mb-1">
<label class="text-xs text-slate-400 font-medium">GitHub Token(選填)</label>
<a href="https://github.com/settings/tokens" target="_blank" class="text-xs text-indigo-400 hover:underline">取得 &rarr;</a>
</div>
<input id="input-gh-token" type="password" placeholder="ghp_xxxxxxxxxxxx"
class="w-full px-3 py-2 bg-slate-950 border border-slate-700 rounded-lg text-sm text-slate-200 focus:outline-none focus:border-indigo-500"/>
<p class="text-[11px] text-slate-600 mt-1">不填時每小時限制 60 次,填入後提升至 5000 次</p>
</div>
</div>
<button onclick="saveSettings()" class="w-full py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg text-sm transition">
儲存設定
</button>
</div>
</div>
</div>
<!-- AI Report Modal -->
<div id="ai-modal" style="display:none;position:fixed;inset:0;z-index:60;background:rgba(0,0,0,.7);align-items:center;justify-content:center;padding:1rem;">
<div class="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-2xl max-h-[85vh] flex flex-col shadow-2xl overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-950/50">
<h3 class="font-bold flex items-center gap-2 text-indigo-400">
✨ AI 專案分析報告
<span id="ai-repo-name" class="text-slate-300 font-normal ml-2"></span>
</h3>
<div class="flex items-center gap-4">
<button id="btn-download-word" onclick="downloadWord()" style="display:none;" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs transition flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
下載 WORD
</button>
<button onclick="closeAiModal()" class="text-slate-500 hover:text-slate-300 text-2xl leading-none">&times;</button>
</div>
</div>
<!-- Body -->
<div class="p-6 overflow-y-auto custom-scrollbar flex-1 flex flex-col" style="background-color: #020817;">
<!-- Step 1: Loading README -->
<div id="ai-step-fetch" style="display:flex;" class="flex-col items-center justify-center py-10 gap-4">
<div class="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
<p class="text-slate-400 text-sm">正在抓取 GitHub README 檔案...</p>
</div>
<!-- Step 2: Confirm Tokens -->
<div id="ai-step-confirm" style="display:none;" class="flex-col items-center text-center py-8 gap-4">
<div class="bg-indigo-950/50 border border-indigo-500/30 rounded-xl p-5 max-w-sm w-full mx-auto">
<svg class="w-8 h-8 text-indigo-400 mx-auto mb-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
<h4 class="text-slate-200 font-medium mb-1">準備就緒</h4>
<p id="ai-token-estimate" class="text-slate-400 text-xs mb-4">README 長度:- 字元(預估約 - tokens)</p>
<button id="btn-confirm-ai" class="w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm transition">
確定消耗 Token 並分析
</button>
</div>
</div>
<!-- Step 3: Analyzing -->
<div id="ai-step-analyzing" style="display:none;" class="flex-col items-center justify-center py-10 gap-4">
<div class="w-6 h-6 border-2 border-fuchsia-500 border-t-transparent rounded-full animate-spin"></div>
<p class="text-fuchsia-400 text-sm animate-pulse">Gemini 正在閱讀並撰寫報告中...</p>
</div>
<!-- Step 4: Report Result -->
<div id="ai-step-result" style="display:none;" class="flex-col gap-4">
<style>
.ai-markdown { color: #cbd5e1; font-size: 19px; line-height: 1.7; }
.ai-markdown h3 { color: #818cf8; font-size: 21px; font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; border-bottom: 1px solid #1e293b; padding-bottom: 4px; }
.ai-markdown ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1em; }
.ai-markdown li { margin-bottom: 0.25em; }
.ai-markdown p { margin-bottom: 1em; }
.ai-markdown strong { color: #f8fafc; font-weight: 600; }
</style>
<div id="ai-report-content" class="ai-markdown"></div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast" style="display:none;position:fixed;bottom:1.25rem;right:1.25rem;z-index:50;" class="bg-slate-800 border border-slate-700 text-slate-100 px-4 py-3 rounded-xl shadow-2xl text-sm">
<span id="toast-msg"></span>
</div>
<script>
// === Global State ===
var GH_TOKEN = '', GEMINI_KEY = '', HISTORY = [], CUR_PAGE = 1, TOTAL = 0, LAST_QUERY = '', BUSY = false;
try {
GH_TOKEN = localStorage.getItem('gh_tok') || '';
GEMINI_KEY = localStorage.getItem('gm_key') || '';
HISTORY = JSON.parse(localStorage.getItem('gh_hist') || '[]');
} catch(e) {}
// === Modal ===
function openModal() {
var elGemini = document.getElementById('input-gemini');
if (elGemini) elGemini.value = GEMINI_KEY;
var elGh = document.getElementById('input-gh-token');
if (elGh) elGh.value = GH_TOKEN;
var elModal = document.getElementById('modal');
if (elModal) elModal.style.display = 'flex';
}
function closeModal() {
var elModal = document.getElementById('modal');
if (elModal) elModal.style.display = 'none';
}
function saveSettings() {
var elGemini = document.getElementById('input-gemini');
if (elGemini) GEMINI_KEY = elGemini.value.trim();
var elGh = document.getElementById('input-gh-token');
if (elGh) GH_TOKEN = elGh.value.trim();
try {
localStorage.setItem('gm_key', GEMINI_KEY);
if (elGh) localStorage.setItem('gh_tok', GH_TOKEN);
} catch(e) {}
closeModal();
toast('已儲存設定!');
}
// === Toast ===
var _toastTimer;
function toast(msg, err) {
var el = document.getElementById('toast');
document.getElementById('toast-msg').textContent = msg;
el.style.background = err ? '#450a0a' : '#1e293b';
el.style.borderColor = err ? '#991b1b' : '#334155';
el.style.display = 'block';
clearTimeout(_toastTimer);
_toastTimer = setTimeout(function(){ el.style.display='none'; }, 3000);
}
// === History ===
function addHistory(term) {
HISTORY = HISTORY.filter(function(t){ return t !== term; });
HISTORY.unshift(term);
if (HISTORY.length > 5) HISTORY = HISTORY.slice(0, 5);
try { localStorage.setItem('gh_hist', JSON.stringify(HISTORY)); } catch(e) {}
renderHistory();
}
function renderHistory() {
var bar = document.getElementById('history-bar');
if (!HISTORY.length) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
var chips = document.getElementById('history-chips');
chips.innerHTML = HISTORY.map(function(t, i) {
return '<button type="button" onclick="pickHistory(' + i + ')" style="padding:2px 10px;background:#1e293b;border:1px solid #334155;color:#94a3b8;border-radius:999px;font-size:11px;cursor:pointer;">' + esc(t) + '</button>';
}).join('');
}
function pickHistory(i) {
document.getElementById('search-query').value = HISTORY[i];
document.getElementById('search-form').dispatchEvent(new Event('submit'));
}
// === Utilities ===
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function isCJK(t) { return /[\u4e00-\u9fff]/.test(t); }
function daysAgo(d) { var dt=new Date(); dt.setDate(dt.getDate()-d); return dt.toISOString().split('T')[0]; }
function fmt(n) { return n>999?(n/1000).toFixed(1)+'k':n; }
function setStatus(msg, spin) {
document.getElementById('status-bar').style.display = 'flex';
document.getElementById('status-text').textContent = msg;
document.getElementById('spinner').style.display = spin ? 'block' : 'none';
}
function showQueryRow(q) {
document.getElementById('query-display').value = q;
document.getElementById('query-row').style.display = 'flex';
}
// === Search Submit ===
function handleSubmit(e) {
e.preventDefault();
var raw = document.getElementById('search-query').value.trim();
if (!raw || BUSY) return;
addHistory(raw);
CUR_PAGE = 1;
doSearch(raw);
}
function reSearch() {
var q = document.getElementById('query-display').value.trim();
if (!q || BUSY) return;
LAST_QUERY = q;
CUR_PAGE = 1;
fetchAndRender();
}
function changePage(d) {
if (BUSY) return;
CUR_PAGE += d;
fetchAndRender();
window.scrollTo({top:0,behavior:'smooth'});
}
function fallbackTranslate(raw) {
return fetch('https://api.mymemory.translated.net/get?q='+encodeURIComponent(raw.slice(0,200))+'&langpair=zh-TW|en')
.then(function(r){ return r.ok?r.json():null; })
.then(function(d){ return (d&&d.responseData&&d.responseData.translatedText)?d.responseData.translatedText.replace(/[^\w\s]/g,' ').trim()||raw : raw; })
.catch(function(){ return raw; });
}
function generateQuery(raw) {
if (!isCJK(raw)) return Promise.resolve(raw);
return fallbackTranslate(raw);
}
function batchTranslate(items) {
if (!GEMINI_KEY || items.length === 0) {
items.forEach(function(item) { item.zhDesc = item.description || ''; });
return Promise.resolve(items);
}
var mapObj = {};
items.forEach(function(item, i) {
if (item.description) mapObj[i] = item.description;
});
if (Object.keys(mapObj).length === 0) {
items.forEach(function(item) { item.zhDesc = ''; });
return Promise.resolve(items);
}
var prompt = "Translate the following JSON object's string values into Traditional Chinese (zh-TW). Keep technical terms in English. Return ONLY a valid JSON object with the exact same keys and the translated strings as values. Do NOT return markdown formatting.\\n\\n" + JSON.stringify(mapObj);
return fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent?key='+GEMINI_KEY, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({contents:[{parts:[{text:prompt}]}]})
})
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(d) {
var out = (d && d.candidates && d.candidates[0] && d.candidates[0].content) ? d.candidates[0].content.parts[0].text.trim() : "";
out = out.replace(/^```json/i, '').replace(/^```/, '').replace(/```$/, '').trim();
var translated = {};
try { translated = JSON.parse(out); } catch(e) { console.error("Batch translate parse error", e, out); }
items.forEach(function(item, i) {
item.zhDesc = translated[i] || item.description || '';
});
return items;
})
.catch(function(e) {
console.error("Batch translate error:", e);
items.forEach(function(item) { item.zhDesc = item.description || ''; });
return items;
});
}
// === Main Search Flow ===
function doSearch(raw) {
document.getElementById('empty-state').style.display = 'block';
document.getElementById('results-grid').style.display = 'none';
document.getElementById('pagination').style.display = 'none';
document.getElementById('result-count').style.display = 'none';
document.getElementById('query-row').style.display = 'none';
setStatus('正在翻譯成英文關鍵字...', true);
generateQuery(raw).then(function(q) {
LAST_QUERY = q;
showQueryRow(q);
return fetchAndRender();
});
}
function fetchAndRender() {
if (BUSY) return Promise.resolve();
BUSY = true;
setStatus('Searching GitHub...', true);
document.getElementById('empty-state').style.display = 'none';
document.getElementById('pagination').style.display = 'none';
var grid = document.getElementById('results-grid');
grid.innerHTML = [0,1,2,3,4,5].map(function(){
return '<div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:12px">'
+ '<div class="sk" style="height:16px;width:60%"></div>'
+ '<div class="sk" style="height:12px;width:100%"></div>'
+ '<div class="sk" style="height:12px;width:80%"></div>'
+ '</div>';
}).join('');
grid.style.display = 'grid';
var q = document.getElementById('query-display').value.trim() || LAST_QUERY;
var lang = document.getElementById('f-lang').value;
var sort = document.getElementById('f-sort').value;
var star = document.getElementById('f-stars').value;
var time = document.getElementById('f-time').value;
var pp = document.getElementById('f-pp').value;
if (lang) q += ' language:'+lang;
if (star) q += ' stars:>'+star;
if (time) q += ' pushed:>'+daysAgo(parseInt(time));
var url = 'https://api.github.com/search/repositories?q='+encodeURIComponent(q)+'&sort='+sort+'&order=desc&per_page='+pp+'&page='+CUR_PAGE;
var headers = {'Accept':'application/vnd.github.v3+json'};
if (GH_TOKEN) headers['Authorization'] = 'token '+GH_TOKEN;
return fetch(url, {headers:headers})
.then(function(r){
if (!r.ok){
if(r.status===403) throw new Error('GitHub API rate limit - please add a GitHub Token in Settings');
if(r.status===422) throw new Error('Search syntax error - please modify the query');
throw new Error('GitHub API error ('+r.status+')');
}
return r.json();
})
.then(function(data){
TOTAL = data.total_count;
var items = data.items||[];
var rc = document.getElementById('result-count');
rc.textContent = TOTAL.toLocaleString()+' results';
rc.style.display = 'inline';
if(!items.length){
grid.style.display='none';
document.getElementById('empty-state').style.display='block';
document.getElementById('empty-title').textContent='No results found';
document.getElementById('empty-desc').textContent='Try modifying the query or relaxing filters';
setStatus('Done - no results', false);
BUSY=false; return;
}
setStatus('正在將結果翻譯為繁體中文...', true);
return batchTranslate(items).then(function(translated) {
setStatus('Done! '+TOTAL.toLocaleString()+' results found.', false);
renderCards(translated);
var totalPages = Math.ceil(TOTAL/parseInt(pp));
if(totalPages>1){
document.getElementById('page-info').textContent='Page '+CUR_PAGE+' / '+Math.min(totalPages,100);
document.getElementById('btn-prev').disabled = CUR_PAGE<=1;
document.getElementById('btn-next').disabled = CUR_PAGE>=totalPages||CUR_PAGE>=100;
document.getElementById('pagination').style.display='flex';
}
BUSY=false;
});
})
.catch(function(err){
console.error(err);
grid.style.display='none';
document.getElementById('empty-state').style.display='block';
document.getElementById('empty-title').textContent='Search failed';
document.getElementById('empty-desc').textContent=err.message;
setStatus('Error', false);
toast(err.message, true);
BUSY=false;
});
}
// === Render Cards ===
function renderCards(items) {
var grid = document.getElementById('results-grid');
grid.innerHTML = items.map(function(item){
var stars = fmt(item.stargazers_count);
var forks = fmt(item.forks_count);
var updated = new Date(item.updated_at).toLocaleDateString('zh-TW');
var topics = (item.topics||[]).slice(0,4).map(function(t){
return '<span style="font-size:10px;background:#1e293b;color:#94a3b8;border:1px solid #334155;padding:1px 6px;border-radius:999px;">'+esc(t)+'</span>';
}).join('');
var cloneUrl = item.clone_url;
var owner = esc(item.owner.login);
var repo = esc(item.name);
return '<div class="fi" style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:10px;transition:border-color .2s;" onmouseover="this.style.borderColor=\'#6366f1\'" onmouseout="this.style.borderColor=\'#1e293b\'">'
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;">'
+ '<div style="display:flex;align-items:center;gap:8px;min-width:0;">'
+ '<img src="'+esc(item.owner.avatar_url)+'" style="width:18px;height:18px;border-radius:50%;flex-shrink:0;" loading="lazy"/>'
+ '<a href="'+esc(item.html_url)+'" target="_blank" rel="noopener" style="font-weight:600;color:#f1f5f9;text-decoration:none;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" onmouseover="this.style.color=\'#818cf8\'" onmouseout="this.style.color=\'#f1f5f9\'">'+esc(item.name)+'</a>'
+ '</div>'
+ (item.language?'<span style="font-size:10px;background:#1e293b;color:#94a3b8;border:1px solid #334155;padding:1px 8px;border-radius:4px;flex-shrink:0;">'+esc(item.language)+'</span>':'')
+ '</div>'
+ '<div style="font-size:11px;color:#475569;">'+esc(item.owner.login)+' &bull; updated '+updated+'</div>'
+ '<p style="font-size:13px;color:#cbd5e1;line-height:1.6;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;">'+esc(item.zhDesc || 'No description provided.')+'</p>'
+ (item.description && item.zhDesc !== item.description ? '<details style="font-size:11px;"><summary style="color:#475569;cursor:pointer;">Original (EN)</summary><p style="color:#64748b;font-style:italic;margin-top:4px;">'+esc(item.description)+'</p></details>' : '')
+ (topics?'<div style="display:flex;flex-wrap:wrap;gap:4px;">'+topics+'</div>':'')
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding-top:8px;border-top:1px solid #1e293b;margin-top:auto;">'
+ '<div style="display:flex;align-items:center;gap:12px;font-size:12px;color:#64748b;">'
+ '<span style="color:#f59e0b;font-weight:500;">★ '+stars+'</span>'
+ '<span>⑂ '+forks+'</span>'
+ '</div>'
+ '<div style="display:flex;gap:4px;">'
+ '<button onclick="openAiAnalysis(\''+owner+'\', \''+repo+'\')" style="padding:4px 8px;color:#818cf8;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.2);cursor:pointer;border-radius:4px;font-size:11px;font-weight:500;display:flex;align-items:center;gap:4px;" title="AI 分析報告" onmouseover="this.style.background=\'rgba(99,102,241,0.2)\'" onmouseout="this.style.background=\'rgba(99,102,241,0.1)\'">'
+ '✨ AI 分析'
+ '</button>'
+ '<button onclick="copyUrl(\'' + cloneUrl.replace(/'/g,"\\'") + '\')" style="padding:4px;color:#475569;background:none;border:none;cursor:pointer;border-radius:4px;" title="Copy Clone URL" onmouseover="this.style.color=\'#f1f5f9\';this.style.background=\'#1e293b\'" onmouseout="this.style.color=\'#475569\';this.style.background=\'none\'">'
+ '<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>'
+ '</button>'
+ '<a href="'+esc(item.html_url)+'" target="_blank" rel="noopener" style="padding:4px;color:#475569;display:flex;border-radius:4px;" title="Open GitHub" onmouseover="this.style.color=\'#f1f5f9\';this.style.background=\'#1e293b\'" onmouseout="this.style.color=\'#475569\';this.style.background=\'none\'">'
+ '<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>'
+ '</a>'
+ '</div>'
+ '</div>'
+ '</div>';
}).join('');
}
function copyUrl(url) {
navigator.clipboard.writeText(url)
.then(function(){ toast('Clone URL copied!'); })
.catch(function(){ toast('Copy failed', true); });
}
// === AI Repo Analysis ===
var currentAiRepo = { owner: '', repo: '', readme: '' };
function openAiAnalysis(owner, repo) {
if (!GEMINI_KEY) {
toast('請先在「設定」中填寫 Gemini API Key', true);
openModal();
return;
}
currentAiRepo.owner = owner;
currentAiRepo.repo = repo;
currentAiRepo.readme = '';
document.getElementById('ai-repo-name').textContent = owner + '/' + repo;
// Show modal and reset state
var modal = document.getElementById('ai-modal');
modal.style.display = 'flex';
document.getElementById('ai-step-fetch').style.display = 'flex';
document.getElementById('ai-step-confirm').style.display = 'none';
document.getElementById('ai-step-analyzing').style.display = 'none';
document.getElementById('ai-step-result').style.display = 'none';
document.getElementById('btn-download-word').style.display = 'none';
// Fetch README
var headers = {'Accept': 'application/vnd.github.v3+json'};
if (GH_TOKEN) headers['Authorization'] = 'token ' + GH_TOKEN;
fetch('https://api.github.com/repos/' + owner + '/' + repo + '/readme', {headers: headers})
.then(function(r) {
if (!r.ok) {
if (r.status === 404) throw new Error('這個專案沒有提供 README 文件,無法產生報告。');
throw new Error('無法取得 README (HTTP ' + r.status + ')');
}
return r.json();
})
.then(function(data) {
if (data.encoding === 'base64') {
// Decode base64 UTF-8 string properly
var bin = atob(data.content);
var bytes = new Uint8Array(bin.length);
for(var i=0; i<bin.length; i++) bytes[i] = bin.charCodeAt(i);
currentAiRepo.readme = new TextDecoder('utf-8').decode(bytes);
} else {
throw new Error('未知的編碼格式');
}
var chars = currentAiRepo.readme.length;
var approxTokens = Math.ceil(chars / 3.5);
document.getElementById('ai-step-fetch').style.display = 'none';
document.getElementById('ai-token-estimate').textContent = 'README 長度:' + chars.toLocaleString() + ' 字元(預估約消耗 ' + approxTokens.toLocaleString() + ' tokens)';
document.getElementById('ai-step-confirm').style.display = 'flex';
var btnConfirm = document.getElementById('btn-confirm-ai');
btnConfirm.onclick = runAiAnalysis;
})
.catch(function(err) {
document.getElementById('ai-step-fetch').style.display = 'none';
document.getElementById('ai-report-content').innerHTML = '<p class="text-red-400">' + err.message + '</p>';
document.getElementById('ai-step-result').style.display = 'flex';
});
}
function closeAiModal() {
document.getElementById('ai-modal').style.display = 'none';
}
function runAiAnalysis() {
if (!GEMINI_KEY) {
closeAiModal();
openModal();
toast('🔑 請先設定 Gemini API Key 以啟用 AI 分析功能!', true);
return;
}
var btnConfirm = document.getElementById('btn-confirm-ai');
if (btnConfirm) {
btnConfirm.disabled = true;
setTimeout(function() { btnConfirm.disabled = false; }, 3000);
}
document.getElementById('ai-step-confirm').style.display = 'none';
document.getElementById('ai-step-analyzing').style.display = 'flex';
var prompt = '你是一個專業的 AI 工程師。請閱讀以下 GitHub 專案的 README,並用繁體中文產生一份快速報告。\n\n'
+ '請包含以下三個段落:\n'
+ '<h3>一段話總結</h3>\n'
+ '<p>(一句話說明這個內容是什麼、解決什麼問題)</p>\n'
+ '<h3>核心功能與特點</h3>\n'
+ '<ul><li>(列出主要特點、架構或用途)</li></ul>\n'
+ '<h3>適用場景</h3>\n'
+ '<ul><li>(列出適合誰用、什麼時候用)</li></ul>\n\n'
+ '注意:請直接輸出帶有 h3, p, ul, li 標籤的 HTML 格式內容,不要使用 markdown 的 `**` 粗體或是 `##` 標題,也不要用 ``` 包裝。\n\n'
+ '專案名稱:' + currentAiRepo.owner + '/' + currentAiRepo.repo + '\n\n'
+ 'README 內容如下:\n'
+ (currentAiRepo.readme || '').substring(0, 3500);
var payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
maxOutputTokens: 2000
}
};
var modelList = ['gemini-3.6-flash', 'gemini-2.0-flash', 'gemini-1.5-flash'];
function tryModel(index) {
if (index >= modelList.length) {
throw new Error('Gemini API 觸發頻率限制 (429 Rate Limit):請至 Google AI Studio 檢查是否已綁定 Pay-As-You-Go 付費信用卡帳號,或稍候 1 分鐘再試。');
}
var modelName = modelList[index];
var url = 'https://generativelanguage.googleapis.com/v1beta/models/' + modelName + ':generateContent?key=' + GEMINI_KEY;
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(function(r) {
if (!r.ok) {
if (r.status === 429 || r.status === 404) {
console.warn('Model ' + modelName + ' returned ' + r.status + ', trying next model...');
return tryModel(index + 1);
}
return r.text().then(function(errText) {
throw new Error('Gemini API 錯誤 (' + r.status + '):' + errText.substring(0, 150));
});
}
return r.json();
});
}
tryModel(0)
.then(function(d) {
document.getElementById('ai-step-analyzing').style.display = 'none';
var out = '';
if (d && d.candidates && d.candidates[0] && d.candidates[0].content) {
out = d.candidates[0].content.parts[0].text.trim();
out = out.replace(/^```html/i, '').replace(/^```/, '').replace(/```$/, '').trim();
document.getElementById('btn-download-word').style.display = 'flex';
} else {
out = '<p class="text-red-400">AI 回傳格式異常:' + JSON.stringify(d).substring(0, 150) + '</p>';
}
document.getElementById('ai-report-content').innerHTML = out;
document.getElementById('ai-step-result').style.display = 'flex';
})
.catch(function(err) {
document.getElementById('ai-step-analyzing').style.display = 'none';
document.getElementById('ai-report-content').innerHTML = '<p class="text-red-400">' + err.message + '</p>';
document.getElementById('ai-step-result').style.display = 'flex';
});
}
// === Global State & Favorites & Text Size ===
var FAVS = {};
var SHOWING_FAVS = false;
try {
FAVS = JSON.parse(localStorage.getItem('hf_favs') || '{}');
} catch(e) {}
function changeTextSize(val) {
document.documentElement.style.fontSize = val;
try { localStorage.setItem('text_size', val); } catch(e) {}
}
function updateFavCount() {
var count = Object.keys(FAVS).length;
var el = document.getElementById('fav-count');
if (el) el.textContent = count;
}
function toggleFav(id, cat, lib) {
if (FAVS[id]) {
delete FAVS[id];
toast('已從收藏夾移除');
} else {
FAVS[id] = { id: id, _cat: cat, library_name: lib, savedAt: Date.now() };
toast('⭐ 已加入收藏夾!');
}
try { localStorage.setItem('hf_favs', JSON.stringify(FAVS)); } catch(e) {}
updateFavCount();
if (SHOWING_FAVS) renderFavs();
else fetchAndRender();
}
function toggleFavsView() {
SHOWING_FAVS = !SHOWING_FAVS;
var btn = document.getElementById('btn-favs');
if (SHOWING_FAVS) {
if (btn) btn.classList.add('ring-2', 'ring-amber-500');
renderFavs();
} else {
if (btn) btn.classList.remove('ring-2', 'ring-amber-500');
fetchAndRender();
}
}
function clearFavs() {
if (!confirm('確定要清空所有收藏嗎?')) return;
FAVS = {};
try { localStorage.removeItem('hf_favs'); } catch(e) {}
updateFavCount();
renderFavs();
toast('已清空所有收藏!');
}
function renderFavs() {
var items = Object.values(FAVS);
var rc = document.getElementById('result-count');
if (rc) {
rc.innerHTML = '收藏庫 (' + items.length + ') ' + (items.length ? '<button onclick="clearFavs()" class="ml-3 px-2 py-0.5 text-xs bg-red-500/20 text-red-300 border border-red-500/30 rounded hover:bg-red-500 hover:text-white transition">🗑️ 清空收藏</button>' : '');
rc.style.display = 'inline';
}
if (!items.length) {
document.getElementById('results-grid').style.display = 'none';
document.getElementById('empty-state').style.display = 'block';
document.getElementById('empty-title').textContent = '收藏庫是空的';
document.getElementById('empty-desc').textContent = '點擊卡片右上角的 ⭐ 即可收藏';
setStatus('Favorites empty', false);
return;
}
setStatus('顯示收藏庫中 (' + items.length + ' 筆)', false);
renderCards(items);
}
function copyPythonCode(e, id, cat, lib) {
if (e && e.stopPropagation) e.stopPropagation();
var code = '';
if (cat === 'dataset') {
code = 'from datasets import load_dataset\n\ndataset = load_dataset("' + id + '")';
} else if (cat === 'space') {
code = 'import gradio as gr\n\ndemo = gr.load("' + id + '", src="spaces")\ndemo.launch()';
} else {
if (lib === 'diffusers') {
code = 'from diffusers import DiffusionPipeline\nimport torch\n\npipeline = DiffusionPipeline.from_pretrained("' + id + '")\n# pipeline.to("cuda")';
} else {
code = 'from transformers import AutoModelForCausalLM, AutoTokenizer\n\ntokenizer = AutoTokenizer.from_pretrained("' + id + '")\nmodel = AutoModelForCausalLM.from_pretrained("' + id + '")';
}
}
navigator.clipboard.writeText(code).then(function() {
toast('📋 已複製 Python 載入程式碼!');
}).catch(function() {
toast('複製失敗', true);
});
}
// Init
try {
var savedSize = localStorage.getItem('text_size');
if (savedSize) {
document.documentElement.style.fontSize = savedSize;
document.getElementById('text-size').value = savedSize;
}
} catch(e) {}
renderHistory();
</script>
</body>
</html>