Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,018 changes: 1,011 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@
}
},
"dependencies": {
"@codemirror/lang-yaml": "^6.1.2",
"@primer/octicons": "^19.15.2",
"codemirror": "^6.0.2",
"codemirror-json-schema": "^0.8.1",
"codemirror-json5": "^1.0.3",
"gh-lang-colors": "^0.1.11",
"webext-base-css": "^2.0.1",
"webext-options-sync": "^4.2.3"
"webext-options-sync": "^4.2.3",
"yaml": "^2.8.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
Expand Down
94 changes: 52 additions & 42 deletions source/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { initCache } from './cache.js';

Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the import statement for consistency with other statements in the file.

Copilot uses AI. Check for mistakes.
async function getClosestN(ids, offset = 0, limit = 10) {
const apiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJyIn0.drJ8F-oa_6UfCpmKdv4Mbng_E8p71UrZAR895gKOOAk";
const url = "https://simrepo.mub.lol/collections/repos/points/recommend";
const url = "https://simrepo.dera.page/collections/repos/points/recommend";

let remainingIds = [...ids];

Expand Down Expand Up @@ -53,52 +53,62 @@ async function getClosestN(ids, offset = 0, limit = 10) {
throw new Error("All provided IDs were invalid or caused errors.");
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'getSimilarRepos') { (async () => {
console.log('Received message to get similar repos:', message);

const ids = message.repoIds;
const offset = Number(message.offset) || 0;
const limit = Number(message.limit) || 10;

const cacheKey = `cache:getSimilarRepos:${ids.sort().join(',')}:${offset}:${limit}`;

// Check cache first
chrome.storage.local.get([cacheKey], async (result) => {
const cached = result[cacheKey];
if (cached) {
console.log('Serving from cache');
sendResponse({ status: 'success', cached: cached.timestamp, data: cached.data });
return;
}
async function handleGetSimilarReposMessage(message, sender, sendResponse) {
console.log('Received message to get similar repos:', message);

if (!ids || ids.length === 0) {
sendResponse({ status: "unknown" });
return;
}
const ids = message.repoIds;
const offset = Number(message.offset) || 0;
const limit = Number(message.limit) || 10;

let similarRepos;
try {
similarRepos = await getClosestN(ids, offset, limit);
} catch (error) {
console.error('Error fetching similar repos:', error);
sendResponse({ status: "error", message: error.message });
return;
}

// Cache the result
chrome.storage.local.set({
[cacheKey]: {
data: similarRepos,
timestamp: Date.now()
}
});
const cacheKey = `cache:getSimilarRepos:${ids.sort().join(',')}:${offset}:${limit}`;

// Check cache first
chrome.storage.local.get([cacheKey], async (result) => {
const cached = result[cacheKey];
if (cached) {
console.log('Serving from cache');
sendResponse({ status: 'success', cached: cached.timestamp, data: cached.data });
return;
}

if (!ids || ids.length === 0) {
sendResponse({ status: "unknown" });
return;
}

let similarRepos;
try {
similarRepos = await getClosestN(ids, offset, limit);
} catch (error) {
console.error('Error fetching similar repos:', error);
sendResponse({ status: "error", message: error.message });
return;
}

sendResponse({
status: similarRepos.length > 0 ? "success" : "error",
// Cache the result
chrome.storage.local.set({
[cacheKey]: {
data: similarRepos,
});
timestamp: Date.now()
}
});

sendResponse({
status: similarRepos.length > 0 ? "success" : "error",
data: similarRepos,
});
});
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'openOptionsPage') {
chrome.runtime.openOptionsPage();
return;
}

if (message.type === 'getSimilarRepos') {
(async () => {
await handleGetSimilarReposMessage(message, sender, sendResponse);
})();

// Tell Chrome this is a synchronous response
Expand Down
14 changes: 13 additions & 1 deletion source/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,17 @@ export function formatNumber(num) {
}

export function loadingSpinner(customClass = "", customStyle = "") {
return `<svg style="box-sizing: content-box; color: var(--color-icon-primary); ${customStyle}" width="32" viewBox="0 0 16 16" fill="none" aria-hidden="true" class="${customClass} flex-1 anim-rotate" height="32"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-opacity="0.25" stroke-width="2" vector-effect="non-scaling-stroke" fill="none"></circle><path d="M15 8a7.002 7.002 0 00-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke"></path></svg>`;
return `<svg style="box-sizing: content-box; color: var(--color-icon-primary); ${customStyle}" width="32" viewBox="0 0 16 16" fill="none" aria-hidden="true" class="${customClass} flex-1 anim-rotate" height="32"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-opacity="0.25" stroke-width="2" vector-effect="non-scaling-stroke" fill="none"></circle><path d="M15 8a7.002 7.002 0 00-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke"></path></svg>`;
}

export function setupSettingsListener() {
const settingsBtn = document.querySelector('#simrepo-settings-btn');
if (settingsBtn) {
settingsBtn.addEventListener('click', (e) => {
e.preventDefault();
chrome.runtime.sendMessage({ type: "openOptionsPage" });
});
} else {
console.warn("No settings button");
}
}
59 changes: 53 additions & 6 deletions source/content-repo.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import GH_LANG_COLORS from 'gh-lang-colors';
import octicons from "@primer/octicons";
import { getSimilarRepos, formatNumber, loadingSpinner } from './common.js';
import { getSimilarRepos, formatNumber, loadingSpinner, setupSettingsListener } from './common.js';
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the import statement. All other imports in this file end with semicolons for consistency.

Copilot uses AI. Check for mistakes.
import { optionsStorage } from './options-storage.js';
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the import statement for consistency with other statements in the file.

Copilot uses AI. Check for mistakes.

var loading = false;
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the variable declaration for consistency with other statements in the file.

Copilot uses AI. Check for mistakes.
var nextOffset = 0;
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the variable declaration for consistency with other statements in the file.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -75,6 +76,9 @@ function getContainerHtml(results) {
<div class="BorderGrid-cell">
<h2 class="h4 mb-3">
Similar repositories
<a href="#" id="simrepo-settings-btn" class="Link--secondary pt-1 pl-2" title="Settings">
${octicons.gear.toSVG()}
</a>
<!-- <span title="${results.length}" data-view-component="true" class="Counter">${formatNumber(results.length)}</span> -->
</h2>

Expand All @@ -92,6 +96,9 @@ function getLoadingContainerHtml() {
<div class="BorderGrid-cell">
<h2 class="h4 mb-3">
Similar repositories
<a href="#" id="simrepo-settings-btn" class="Link--secondary pt-1 pl-2" title="Settings">
${octicons.gear.toSVG()}
</a>
</h2>

<div class="d-flex align-items-center justify-content-star mt-3">
Expand All @@ -112,6 +119,9 @@ function getErrorContainerHtml(error = "No similar repositories found.") {
<div class="BorderGrid-cell">
<h2 class="h4 mb-3">
Similar repositories
<a href="#" id="simrepo-settings-btn" class="Link--secondary pt-1 pl-2" title="Settings">
${octicons.gear.toSVG()}
</a>
</h2>

<div class="text-small color-fg-muted">
Expand All @@ -134,6 +144,8 @@ function setupCallback() {
}
});
}

setupSettingsListener();
}

async function getRepoId() {
Expand All @@ -148,6 +160,12 @@ async function getRepoId() {
}

export async function loadMoreRepos(resetOffset = false) {
let options = await optionsStorage.getAll();
if (!options.similarEnabled) {
console.log("Similar repositories are disabled");
return;
}

if (resetOffset) {
nextOffset = 0;
}
Expand All @@ -160,15 +178,33 @@ export async function loadMoreRepos(resetOffset = false) {
if (!container) {
const sidebar = document.querySelector('.Layout-sidebar > div');
sidebar.insertAdjacentHTML('beforeend', getLoadingContainerHtml());
setupSettingsListener();
container = document.querySelector('#similar-repos-container');
}

// Don't fetch if private
const isPrivate = document.querySelector('.Label.Label--secondary')?.textContent.trim() === 'Private';
if (isPrivate) {
if (options.similarShowUnavailable) {
container.outerHTML = getErrorContainerHtml("Unavailable for private repositories.");
setupSettingsListener();
} else {
container.remove();
}
return;
}

// Don't fetch if less than 150 stars
try {
let starSpan = document.querySelector("span[id=\"repo-stars-counter-star\"]");
let starsCount = starSpan ? parseInt(starSpan.getAttribute("title").replace(/,/g, '')) : 0;
if (starsCount < 150) {
container.outerHTML = getErrorContainerHtml("Unavailable for repositories with less than 150 stars.");
if (options.similarShowUnavailable) {
container.outerHTML = getErrorContainerHtml("Unavailable for repositories with less than 150 stars.");
setupSettingsListener();
} else {
container.remove();
}
return;
}
} catch (error) {
Expand All @@ -178,8 +214,9 @@ export async function loadMoreRepos(resetOffset = false) {
try {
loading = true;
let offset = nextOffset;
nextOffset += 5;
let response = await getSimilarRepos([repoId], offset, 5);
let limit = options.similarCount;
nextOffset += limit;
let response = await getSimilarRepos([repoId], offset, limit);
loading = false;

if (response.status === "success" && response.data !== undefined) {
Expand All @@ -193,7 +230,11 @@ export async function loadMoreRepos(resetOffset = false) {

let innerContainer = document.getElementById("similar-repos-inner-container");
if (innerContainer) {
innerContainer.insertAdjacentHTML('beforeend', getContainerInnerHtml(response.data));
if (offset === 0) {
innerContainer.innerHTML = getContainerInnerHtml(response.data);
} else {
innerContainer.insertAdjacentHTML('beforeend', getContainerInnerHtml(response.data));
}
} else {
container.outerHTML = getContainerHtml(response.data);
setupCallback();
Expand All @@ -202,14 +243,20 @@ export async function loadMoreRepos(resetOffset = false) {
console.log('No similar repos found');

if (response.status === "error" && response.message) {
container.outerHTML = getErrorContainerHtml(`Error fetching similar repositories. Details:<br><code>${response.message}</code>`);
if (response.message === "All provided IDs were invalid or caused errors.") {
container.outerHTML = getErrorContainerHtml("This repository got popular too recently! It will be included in the dataset soon.");
} else {
container.outerHTML = getErrorContainerHtml(`Error fetching similar repositories. Details:<br><code>${response.message}</code>`);
}
} else {
container.outerHTML = getErrorContainerHtml("No similar repositories found. Try on older repositories.");
}
setupSettingsListener();
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setupSettingsListener function is called after modifying outerHTML, which replaces the entire element in the DOM. This means the settings button that was just added is no longer in the DOM and the event listener won't be attached. Consider calling setupSettingsListener() after the element has been replaced, or refactor to avoid replacing the entire container.

Copilot uses AI. Check for mistakes.
}
} catch (error) {
console.error('Error fetching similar repos:', error);
loading = false;
container.outerHTML = getErrorContainerHtml(`Error fetching similar repositories. Details:<br><code> ${error.message}</code>`);
setupSettingsListener();
}
}
Loading
Loading