Github-AI-Reviewer / app /static /index.html
SENODROOM
fixed
1ca4563
Raw
History Blame Contribute Delete
12.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Profile AI Reviewer</title>
<style>
:root {
color-scheme: dark;
--bg: #0b0f13;
--panel: #121820;
--panel-soft: #18212b;
--line: #263241;
--text: #eef3f8;
--muted: #93a4b7;
--accent: #3fbf8f;
--accent-strong: #54d6a5;
--warn: #f7c56b;
--danger: #ff7b7b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
header {
border-bottom: 1px solid var(--line);
background: rgba(11, 15, 19, 0.94);
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: blur(12px);
}
.nav {
align-items: center;
display: flex;
gap: 18px;
justify-content: space-between;
margin: 0 auto;
max-width: 1120px;
padding: 16px 20px;
}
.brand {
align-items: center;
display: flex;
gap: 10px;
font-weight: 700;
}
.mark {
align-items: center;
aspect-ratio: 1;
background: var(--accent);
border-radius: 6px;
color: #06110d;
display: inline-flex;
font-weight: 900;
justify-content: center;
width: 32px;
}
.pill {
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-size: 13px;
padding: 7px 12px;
white-space: nowrap;
}
main {
margin: 0 auto;
max-width: 1120px;
padding: 32px 20px 56px;
}
.workspace {
display: grid;
gap: 20px;
grid-template-columns: minmax(0, 1fr) 360px;
align-items: start;
}
.hero {
padding: 14px 0 22px;
}
h1 {
font-size: clamp(34px, 6vw, 62px);
letter-spacing: 0;
line-height: 1.02;
margin: 0 0 16px;
max-width: 780px;
}
.lead {
color: var(--muted);
font-size: 18px;
line-height: 1.6;
margin: 0;
max-width: 720px;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 18px;
}
.analyzer {
display: grid;
gap: 16px;
}
label {
color: var(--muted);
display: block;
font-size: 13px;
font-weight: 700;
margin-bottom: 8px;
text-transform: uppercase;
}
.input-row {
display: grid;
gap: 10px;
grid-template-columns: minmax(0, 1fr) auto;
}
input {
background: #0e141a;
border: 1px solid var(--line);
border-radius: 6px;
color: var(--text);
font: inherit;
min-height: 46px;
outline: none;
padding: 0 13px;
width: 100%;
}
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(63, 191, 143, 0.16);
}
button {
background: var(--accent);
border: 0;
border-radius: 6px;
color: #05110d;
cursor: pointer;
font: inherit;
font-weight: 800;
min-height: 46px;
padding: 0 18px;
white-space: nowrap;
}
button:disabled {
cursor: wait;
opacity: 0.62;
}
.result-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.metric {
background: var(--panel-soft);
border: 1px solid var(--line);
border-radius: 8px;
min-height: 92px;
padding: 14px;
}
.metric span {
color: var(--muted);
display: block;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.metric strong {
display: block;
font-size: 30px;
line-height: 1;
margin-top: 10px;
overflow-wrap: anywhere;
}
.score strong {
color: var(--accent-strong);
font-size: 48px;
}
.status {
color: var(--muted);
min-height: 22px;
}
.error {
color: var(--danger);
}
.language-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 4px;
}
.language-list span {
background: #0e141a;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-size: 13px;
padding: 7px 10px;
}
pre {
background: #080c10;
border: 1px solid var(--line);
border-radius: 8px;
color: #b7c7d8;
font-size: 13px;
line-height: 1.55;
margin: 0;
overflow-x: auto;
padding: 14px;
}
.side {
display: grid;
gap: 20px;
}
.side h2,
.panel h2 {
font-size: 16px;
margin: 0 0 12px;
}
.api-base {
margin-bottom: 14px;
}
.small {
color: var(--muted);
font-size: 13px;
line-height: 1.5;
margin: 10px 0 0;
}
@media (max-width: 860px) {
.workspace {
grid-template-columns: 1fr;
}
.result-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.nav {
align-items: flex-start;
flex-direction: column;
}
.input-row {
grid-template-columns: 1fr;
}
button {
width: 100%;
}
}
</style>
</head>
<body>
<header>
<div class="nav">
<div class="brand"><span class="mark">G</span> GitHub Profile AI Reviewer</div>
<div class="pill" id="health-pill">API status: checking</div>
</div>
</header>
<main>
<section class="hero">
<h1>Rate any public GitHub profile out of 100.</h1>
<p class="lead">Submit a username and the API returns a developer rating, level, contribution signals, language mix, and scoring metadata. A server token is optional.</p>
</section>
<section class="workspace">
<div class="panel analyzer">
<form id="analyze-form">
<label for="username">GitHub username</label>
<div class="input-row">
<input id="username" name="username" value="octocat" autocomplete="off" maxlength="39" pattern="[A-Za-z0-9-]+" required>
<button id="submit-button" type="submit">Analyze</button>
</div>
</form>
<div class="status" id="status">Ready.</div>
<div class="result-grid">
<div class="metric score">
<span>Rating</span>
<strong id="rating">--</strong>
</div>
<div class="metric">
<span>Level</span>
<strong id="level">--</strong>
</div>
<div class="metric">
<span>Language</span>
<strong id="language">--</strong>
</div>
<div class="metric">
<span>Public Commits</span>
<strong id="public-commits">--</strong>
</div>
<div class="metric">
<span>Public PRs Created</span>
<strong id="public-prs">--</strong>
</div>
</div>
<div class="panel">
<h2>Language Breakdown</h2>
<div class="language-list" id="language-breakdown"></div>
</div>
<div class="panel">
<h2>Raw API Response</h2>
<pre id="response">{}</pre>
</div>
</div>
<aside class="side">
<div class="panel">
<h2>API Base</h2>
<input class="api-base" id="api-base" value="">
<p class="small">Leave this as the current origin when this page is served by FastAPI. Use `http://localhost:8000` if opening the HTML file directly.</p>
</div>
<div class="panel">
<h2>Fetch Example</h2>
<pre id="fetch-example"></pre>
</div>
<div class="panel">
<h2>Endpoint</h2>
<pre>POST /analyze
Content-Type: application/json
{
"username": "octocat"
}</pre>
</div>
</aside>
</section>
</main>
<script>
const form = document.getElementById("analyze-form");
const usernameInput = document.getElementById("username");
const apiBaseInput = document.getElementById("api-base");
const submitButton = document.getElementById("submit-button");
const statusText = document.getElementById("status");
const healthPill = document.getElementById("health-pill");
const responseBox = document.getElementById("response");
const fetchExample = document.getElementById("fetch-example");
const fields = {
rating: document.getElementById("rating"),
level: document.getElementById("level"),
language: document.getElementById("language"),
publicCommits: document.getElementById("public-commits"),
publicPrs: document.getElementById("public-prs"),
breakdown: document.getElementById("language-breakdown")
};
function defaultApiBase() {
if (window.location.protocol === "file:" || window.location.port === "5500") {
return "http://127.0.0.1:8000";
}
return window.location.origin;
}
apiBaseInput.value = defaultApiBase();
function endpoint(path) {
return `${apiBaseInput.value.replace(/\/$/, "")}${path}`;
}
function setStatus(message, isError = false) {
statusText.textContent = message;
statusText.classList.toggle("error", isError);
}
function updateFetchExample() {
const username = usernameInput.value || "octocat";
fetchExample.textContent = `const response = await fetch("${endpoint("/analyze")}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "${username}" })
});
const profileRating = await response.json();`;
}
function renderResult(data) {
fields.rating.textContent = Number.isFinite(data.rating_score) ? `${data.rating_score}` : "--";
fields.level.textContent = data.developer_level || "--";
fields.language.textContent = data.strongest_language || "--";
fields.publicCommits.textContent = data.public_activity ? `${data.public_activity.public_commits}` : "--";
fields.publicPrs.textContent = data.public_activity ? `${data.public_activity.public_prs_created}` : "--";
fields.breakdown.innerHTML = "";
const entries = Object.entries(data.language_breakdown || {});
if (!entries.length) {
const empty = document.createElement("span");
empty.textContent = "No language data";
fields.breakdown.appendChild(empty);
}
for (const [language, percent] of entries) {
const item = document.createElement("span");
item.textContent = `${language}: ${percent}%`;
fields.breakdown.appendChild(item);
}
responseBox.textContent = JSON.stringify(data, null, 2);
}
async function checkHealth() {
try {
const response = await fetch(endpoint("/health"));
healthPill.textContent = response.ok ? "API status: online" : "API status: unavailable";
} catch {
healthPill.textContent = "API status: offline";
}
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const username = usernameInput.value.trim();
if (!username) {
setStatus("Enter a GitHub username.", true);
return;
}
submitButton.disabled = true;
setStatus(`Analyzing ${username}...`);
updateFetchExample();
try {
const response = await fetch(endpoint("/analyze"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username })
});
const text = await response.text();
const data = text ? JSON.parse(text) : {};
if (!response.ok) {
const error = new Error(data.detail || "Request failed.");
error.isHttpError = true;
throw error;
}
renderResult(data);
setStatus(`Finished ${username}.`);
} catch (error) {
const networkHint = error instanceof TypeError && !error.isHttpError
? " Check API Base and confirm FastAPI is running on port 8000."
: "";
setStatus(`${error.message}${networkHint}`, true);
} finally {
submitButton.disabled = false;
}
});
usernameInput.addEventListener("input", updateFetchExample);
apiBaseInput.addEventListener("input", () => {
updateFetchExample();
checkHealth();
});
updateFetchExample();
checkHealth();
</script>
</body>
</html>