This commit is contained in:
2026-09-12 21:55:40 +02:00
parent 53055ea5c6
commit 90860b9279
24 changed files with 1958 additions and 45 deletions
+92 -12
View File
@@ -2328,7 +2328,9 @@ const f0cklib = {
mime,
exclude_ids,
session_tags = '',
session_creators = ''
session_creators = '',
continuation = false,
prefer_personalized = null
} = {}) => {
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
const modequery = computeBaseMode(mode, ratingsArr, session);
@@ -2420,13 +2422,30 @@ const f0cklib = {
});
}
// 2. Personalization vs Serendipity Split (60% personalized, 40% random exploration)
// 2. Personalization vs Serendipity Split
// In the sidebar suggestions:
// - 1 of the first 3 recommendations is a personalized recommendation and the other 2 are not.
// - After that, recommendations appear sporadically (~20% rate, non-consecutive with 3-5 random items between them).
const isFirstBatch = !continuation && excludeItemIds.length === 0;
let personalizedTarget;
if (maxLimit === 1) {
// For single-card replacement: 65% chance personalized, 35% chance discovery
personalizedTarget = Math.random() < 0.65 ? 1 : 0;
if (prefer_personalized === true) {
personalizedTarget = 1;
} else if (prefer_personalized === false) {
personalizedTarget = 0;
} else {
personalizedTarget = Math.random() < 0.20 ? 1 : 0;
}
} else if (isFirstBatch) {
// 1 in top 3, plus sporadic recommendations in the remaining slots
const topRec = maxLimit >= 1 ? 1 : 0;
const remainingSlots = Math.max(0, maxLimit - 3);
const sporadicRecs = Math.round(remainingSlots * 0.20);
personalizedTarget = topRec + sporadicRecs;
} else {
personalizedTarget = Math.round(maxLimit * 0.60);
// Continuation batch: sporadic recommendations throughout
personalizedTarget = Math.max(1, Math.round(maxLimit * 0.20));
}
const targetTagIds = targetTagEntries.map(([tid]) => tid);
@@ -2537,8 +2556,9 @@ const f0cklib = {
let randomItems = [];
if (neededRandom > 0) {
const allExclude = [...excludeItemIds, ...personalizedItems.map(p => p.id)];
// Fetch extra buffer of random items to ensure ample spacing
randomItems = await f0cklib.getRandomRecommendations({
limit: neededRandom,
limit: Math.max(neededRandom + 3, maxLimit),
mode,
ratings,
session,
@@ -2550,14 +2570,74 @@ const f0cklib = {
});
}
// 4. Combine & Interweave with Fisher-Yates Shuffle
const combined = [...personalizedItems, ...randomItems];
for (let i = combined.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[combined[i], combined[j]] = [combined[j], combined[i]];
// 4. Combine & Interweave
if (maxLimit === 1) {
return personalizedItems.length > 0 ? personalizedItems.slice(0, 1) : randomItems.slice(0, 1);
}
return combined.slice(0, maxLimit);
if (personalizedItems.length === 0) {
return randomItems.slice(0, maxLimit);
}
if (randomItems.length === 0) {
return personalizedItems.slice(0, maxLimit);
}
const result = [];
if (isFirstBatch) {
// First 3 items: exactly 1 is a recommendation, and the other 2 are not
const topCount = Math.min(3, maxLimit);
const topSlots = new Array(topCount);
const recSlot = Math.floor(Math.random() * topCount);
topSlots[recSlot] = personalizedItems.shift();
for (let s = 0; s < topCount; s++) {
if (s !== recSlot) {
topSlots[s] = randomItems.length > 0 ? randomItems.shift() : personalizedItems.shift();
}
}
for (let s = 0; s < topCount; s++) {
if (topSlots[s]) result.push(topSlots[s]);
}
// Remaining slots: recommendations come sporadically (separated by 3 to 5 non-recommendations)
let gapSinceRec = topCount - 1 - recSlot;
let targetGap = Math.floor(Math.random() * 3) + 3; // 3, 4, or 5 random items
while (result.length < maxLimit && (personalizedItems.length > 0 || randomItems.length > 0)) {
if (personalizedItems.length > 0 && gapSinceRec >= targetGap && randomItems.length > 0) {
result.push(personalizedItems.shift());
gapSinceRec = 0;
targetGap = Math.floor(Math.random() * 3) + 3;
} else if (randomItems.length > 0) {
result.push(randomItems.shift());
gapSinceRec++;
} else if (personalizedItems.length > 0) {
result.push(personalizedItems.shift());
gapSinceRec = 0;
}
}
} else {
// Continuation batch: recommendations come sporadically throughout
let gapSinceRec = Math.floor(Math.random() * 2) + 1;
let targetGap = Math.floor(Math.random() * 3) + 3;
while (result.length < maxLimit && (personalizedItems.length > 0 || randomItems.length > 0)) {
if (personalizedItems.length > 0 && gapSinceRec >= targetGap && randomItems.length > 0) {
result.push(personalizedItems.shift());
gapSinceRec = 0;
targetGap = Math.floor(Math.random() * 3) + 3;
} else if (randomItems.length > 0) {
result.push(randomItems.shift());
gapSinceRec++;
} else if (personalizedItems.length > 0) {
result.push(personalizedItems.shift());
gapSinceRec = 0;
}
}
}
return result.slice(0, maxLimit);
},
getTagFeedItems: async ({