|
2 | 2 | const form = document.getElementById("submit-form"); |
3 | 3 | const btn = document.getElementById("submit-btn"); |
4 | 4 | const banner = document.getElementById("error-banner"); |
| 5 | + const prEl = document.getElementById("pr"); |
5 | 6 | const commentEl = document.getElementById("comment"); |
6 | 7 | const providerEl = document.getElementById("llm-provider"); |
7 | 8 | const modelEl = document.getElementById("llm-model"); |
8 | 9 | const baseUrlEl = document.getElementById("llm-base-url"); |
9 | 10 | const customBaseRow = document.getElementById("custom-base-row"); |
| 11 | + const providerHint = document.getElementById("provider-hint"); |
10 | 12 | const jobsSection = document.getElementById("jobs-section"); |
11 | 13 | const jobsTbody = document.getElementById("jobs-tbody"); |
12 | 14 | const jobsCount = document.getElementById("jobs-count"); |
|
210 | 212 | } |
211 | 213 | } |
212 | 214 |
|
| 215 | + // Mirrors the server-side _parse_pr_ref shapes: full GitHub URL, |
| 216 | + // "owner/repo#NN", or "owner/repo/pull/NN". Returns null when the |
| 217 | + // string isn't yet a parseable PR reference (e.g. half-typed). |
| 218 | + function parseOwnerRepo(raw) { |
| 219 | + const s = (raw || "").trim(); |
| 220 | + if (!s) return null; |
| 221 | + const name = "[A-Za-z0-9._-]+"; |
| 222 | + let m; |
| 223 | + m = s.match(new RegExp(`github\\.com/(${name})/(${name})/(?:pull|pulls)/\\d+`)); |
| 224 | + if (m) return { owner: m[1], repo: m[2] }; |
| 225 | + m = s.match(new RegExp(`^(${name})/(${name})#\\d+`)); |
| 226 | + if (m) return { owner: m[1], repo: m[2] }; |
| 227 | + m = s.match(new RegExp(`^(${name})/(${name})/pull/\\d+`)); |
| 228 | + if (m) return { owner: m[1], repo: m[2] }; |
| 229 | + return null; |
| 230 | + } |
| 231 | + |
| 232 | + let lookupTimer = 0; |
| 233 | + let lastLookupKey = ""; |
| 234 | + |
| 235 | + function scheduleProviderLookup() { |
| 236 | + clearTimeout(lookupTimer); |
| 237 | + lookupTimer = setTimeout(runProviderLookup, 250); |
| 238 | + } |
| 239 | + |
| 240 | + async function runProviderLookup() { |
| 241 | + const parsed = parseOwnerRepo(prEl.value); |
| 242 | + if (!parsed) { |
| 243 | + providerHint.textContent = ""; |
| 244 | + lastLookupKey = ""; |
| 245 | + return; |
| 246 | + } |
| 247 | + const key = `${parsed.owner}/${parsed.repo}`.toLowerCase(); |
| 248 | + if (key === lastLookupKey) return; |
| 249 | + lastLookupKey = key; |
| 250 | + try { |
| 251 | + const qs = new URLSearchParams({ owner: parsed.owner, repo: parsed.repo }); |
| 252 | + const r = await fetch(`/reviews/lookup-provider?${qs}`); |
| 253 | + if (!r.ok) { |
| 254 | + providerHint.textContent = ""; |
| 255 | + return; |
| 256 | + } |
| 257 | + const data = await r.json(); |
| 258 | + if (!data.match) { |
| 259 | + providerHint.textContent = `No provider config matches ${parsed.owner}/${parsed.repo}. Add one at /admin or your submission will be refused.`; |
| 260 | + return; |
| 261 | + } |
| 262 | + applyMatchedConfig(data.match, parsed); |
| 263 | + } catch { |
| 264 | + // Non-fatal — the user can still submit; the server will reject |
| 265 | + // with the same message if no config matches. |
| 266 | + providerHint.textContent = ""; |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + function applyMatchedConfig(match, parsed) { |
| 271 | + providerEl.value = match.provider; |
| 272 | + if (match.default_model) { |
| 273 | + modelEl.value = match.default_model; |
| 274 | + } |
| 275 | + if (match.provider === "custom" && match.api_base) { |
| 276 | + baseUrlEl.value = match.api_base; |
| 277 | + } |
| 278 | + updateProviderFields(); |
| 279 | + const modelPart = match.default_model ? ` · model ${match.default_model}` : ""; |
| 280 | + const scope = |
| 281 | + match.repo_pattern && match.repo_pattern !== `${parsed.owner}/${parsed.repo}` |
| 282 | + ? ` (via ${match.repo_pattern})` |
| 283 | + : ""; |
| 284 | + providerHint.textContent = `Using ${match.provider}${modelPart}${scope}.`; |
| 285 | + } |
| 286 | + |
213 | 287 | form.addEventListener("submit", async (e) => { |
214 | 288 | e.preventDefault(); |
215 | 289 | btn.disabled = true; |
|
254 | 328 | modelEl.addEventListener("change", saveLlmPrefs); |
255 | 329 | baseUrlEl.addEventListener("change", saveLlmPrefs); |
256 | 330 |
|
| 331 | + // Auto-fill provider/model from the matching DB config whenever the |
| 332 | + // PR field changes. Debounced so we don't hit the endpoint on every |
| 333 | + // keystroke, and short-circuited by lastLookupKey when the |
| 334 | + // owner/repo hasn't actually changed. |
| 335 | + prEl.addEventListener("input", scheduleProviderLookup); |
| 336 | + prEl.addEventListener("change", runProviderLookup); |
| 337 | + |
257 | 338 | loadLlmOptions(); |
258 | 339 | loadJobs(); |
| 340 | + // Catch the case where the input already has a value at load time |
| 341 | + // (e.g. browser autofill or a prefilled paste). |
| 342 | + runProviderLookup(); |
259 | 343 | // Soft-refresh every 5s so running reviews tick over to "done" / |
260 | 344 | // "published" without the user having to reload. |
261 | 345 | setInterval(loadJobs, 5000); |
|
0 commit comments