2026-06-18 22:48:16 +08:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="zh-CN" class="h-full">
|
|
|
|
|
{{template "head" .}}
|
|
|
|
|
<body class="h-full bg-gray-50 text-gray-900">
|
|
|
|
|
<div class="min-h-full flex flex-col">
|
|
|
|
|
{{template "nav" .}}
|
|
|
|
|
<main class="flex-1">
|
|
|
|
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
|
|
|
<!-- Hero -->
|
|
|
|
|
<div class="text-center mb-10">
|
|
|
|
|
<h1 class="text-3xl font-bold text-gray-900 mb-3">PR-Helper</h1>
|
|
|
|
|
<p class="text-gray-500">输入 Git 仓库 URL,自动生成 PR 描述并进行 AI 代码审查</p>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Clone Form -->
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
|
|
|
|
<h2 class="text-lg font-semibold mb-4">克隆仓库</h2>
|
|
|
|
|
<form id="clone-form" class="flex gap-3">
|
|
|
|
|
<input type="text" id="repo-url" name="url" placeholder="https://github.com/user/repo.git"
|
|
|
|
|
class="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" required>
|
|
|
|
|
<button type="submit" id="clone-btn"
|
|
|
|
|
class="bg-indigo-600 text-white px-5 py-2 rounded-md text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
|
|
|
|
克隆
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
<div id="clone-progress" class="hidden mt-4">
|
|
|
|
|
<div class="flex items-center gap-3 mb-2">
|
|
|
|
|
<div class="animate-spin rounded-full h-4 w-4 border-2 border-indigo-600 border-t-transparent"></div>
|
|
|
|
|
<span id="clone-status" class="text-sm text-gray-600">准备中...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
|
|
|
|
<div id="clone-bar" class="bg-indigo-600 h-2 rounded-full transition-all duration-300" style="width:0%"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="clone-error" class="hidden mt-3 text-sm text-red-600"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Cached Repos -->
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
|
|
|
<div class="flex justify-between items-center mb-4">
|
|
|
|
|
<h2 class="text-lg font-semibold">已缓存仓库</h2>
|
|
|
|
|
<button hx-post="/api/repos/cleanup" hx-swap="none"
|
|
|
|
|
class="text-sm text-gray-500 hover:text-red-600"
|
|
|
|
|
hx-on::after-request="window.location.reload()">
|
|
|
|
|
清理过期缓存
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="repos-list">
|
|
|
|
|
{{if .Repos}}
|
|
|
|
|
<div class="divide-y divide-gray-100">
|
|
|
|
|
{{range .Repos}}
|
|
|
|
|
<div class="py-3 flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<a href="/repo/{{.ID}}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">{{.URL}}</a>
|
|
|
|
|
<p class="text-xs text-gray-400 mt-1">最后使用: {{.LastUsed}}</p>
|
|
|
|
|
</div>
|
2026-06-21 14:31:33 +08:00
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button hx-post="/api/repos/{{.ID}}/pull" hx-swap="none"
|
|
|
|
|
hx-on::before-request="this.disabled=true;this.textContent='更新中...'"
|
|
|
|
|
hx-on::after-request="window.location.reload()"
|
|
|
|
|
class="text-xs text-indigo-500 hover:text-indigo-700">更新</button>
|
|
|
|
|
<button hx-delete="/api/repos/{{.ID}}" hx-confirm="确定删除此仓库缓存?"
|
|
|
|
|
hx-swap="none" hx-on::after-request="window.location.reload()"
|
|
|
|
|
class="text-xs text-red-500 hover:text-red-700">删除</button>
|
|
|
|
|
</div>
|
2026-06-18 22:48:16 +08:00
|
|
|
</div>
|
|
|
|
|
{{end}}
|
|
|
|
|
</div>
|
|
|
|
|
{{else}}
|
|
|
|
|
<p class="text-sm text-gray-400 text-center py-6">暂无缓存仓库</p>
|
|
|
|
|
{{end}}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
{{template "footer" .}}
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
document.getElementById('clone-form').addEventListener('submit', async function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const url = document.getElementById('repo-url').value.trim();
|
|
|
|
|
if (!url) return;
|
|
|
|
|
const btn = document.getElementById('clone-btn');
|
|
|
|
|
const progress = document.getElementById('clone-progress');
|
|
|
|
|
const status = document.getElementById('clone-status');
|
|
|
|
|
const bar = document.getElementById('clone-bar');
|
|
|
|
|
const errorEl = document.getElementById('clone-error');
|
|
|
|
|
btn.disabled = true;
|
2026-06-18 23:04:58 +08:00
|
|
|
btn.textContent = '克隆中...';
|
2026-06-18 22:48:16 +08:00
|
|
|
progress.classList.remove('hidden');
|
|
|
|
|
errorEl.classList.add('hidden');
|
|
|
|
|
bar.style.width = '0%';
|
2026-06-18 23:04:58 +08:00
|
|
|
status.textContent = '正在连接...';
|
|
|
|
|
|
2026-06-18 22:48:16 +08:00
|
|
|
try {
|
|
|
|
|
const resp = await fetch('/api/repos', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
2026-06-20 21:57:46 +08:00
|
|
|
credentials: 'same-origin',
|
2026-06-18 22:48:16 +08:00
|
|
|
body: JSON.stringify({url: url})
|
|
|
|
|
});
|
2026-06-18 23:04:58 +08:00
|
|
|
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
throw new Error(data.error || 'clone failed');
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 22:48:16 +08:00
|
|
|
const reader = resp.body.getReader();
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
let buffer = '';
|
2026-06-18 23:04:58 +08:00
|
|
|
let currentEvent = '';
|
|
|
|
|
|
2026-06-18 22:48:16 +08:00
|
|
|
while (true) {
|
|
|
|
|
const {done, value} = await reader.read();
|
|
|
|
|
if (done) break;
|
2026-06-18 23:04:58 +08:00
|
|
|
|
2026-06-18 22:48:16 +08:00
|
|
|
buffer += decoder.decode(value, {stream: true});
|
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
|
buffer = lines.pop() || '';
|
2026-06-18 23:04:58 +08:00
|
|
|
|
2026-06-18 22:48:16 +08:00
|
|
|
for (const line of lines) {
|
2026-06-18 23:04:58 +08:00
|
|
|
if (line.startsWith('event: ')) {
|
|
|
|
|
currentEvent = line.slice(7).trim();
|
|
|
|
|
} else if (line.startsWith('data: ')) {
|
2026-06-18 22:48:16 +08:00
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(line.slice(6));
|
2026-06-18 23:04:58 +08:00
|
|
|
|
|
|
|
|
if (currentEvent === 'start') {
|
|
|
|
|
status.textContent = '开始克隆...';
|
|
|
|
|
bar.style.width = '10%';
|
|
|
|
|
} else if (currentEvent === 'progress') {
|
|
|
|
|
status.textContent = data.step || '处理中...';
|
|
|
|
|
if (data.current !== undefined && data.total > 0) {
|
|
|
|
|
bar.style.width = Math.round((data.current / data.total) * 80 + 10) + '%';
|
|
|
|
|
} else {
|
|
|
|
|
bar.style.width = '50%';
|
|
|
|
|
}
|
|
|
|
|
} else if (currentEvent === 'complete') {
|
|
|
|
|
bar.style.width = '100%';
|
|
|
|
|
status.textContent = '克隆完成!';
|
|
|
|
|
// Redirect to repo page
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
window.location.href = '/repo/' + data.repo_id;
|
|
|
|
|
}, 500);
|
|
|
|
|
return;
|
|
|
|
|
} else if (currentEvent === 'error') {
|
|
|
|
|
throw new Error(data.message || '克隆失败');
|
|
|
|
|
}
|
|
|
|
|
} catch (parseErr) {
|
|
|
|
|
if (parseErr.message && !parseErr.message.includes('JSON')) {
|
|
|
|
|
throw parseErr;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-18 22:48:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
errorEl.textContent = err.message;
|
|
|
|
|
errorEl.classList.remove('hidden');
|
2026-06-18 23:04:58 +08:00
|
|
|
progress.classList.add('hidden');
|
2026-06-18 22:48:16 +08:00
|
|
|
} finally {
|
|
|
|
|
btn.disabled = false;
|
2026-06-18 23:04:58 +08:00
|
|
|
btn.textContent = '克隆';
|
2026-06-18 22:48:16 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|