add new lesson mode and started code refraction
This commit is contained in:
@@ -83,3 +83,22 @@ export const getQueue = async (user, limit = 100, sortMode) => {
|
||||
}
|
||||
return dueItems;
|
||||
};
|
||||
|
||||
export const getLessonQueue = async (user, limit = 100) => {
|
||||
const query = {
|
||||
userId: user._id,
|
||||
srsLevel: 0
|
||||
};
|
||||
return await StudyItem.find(query).sort({ level: 1, wkSubjectId: 1 }).limit(limit);
|
||||
};
|
||||
|
||||
export const processLesson = async (user, subjectId) => {
|
||||
const item = await StudyItem.findOne({ userId: user._id, wkSubjectId: subjectId });
|
||||
if (!item) throw new Error('Item not found');
|
||||
|
||||
item.srsLevel = 1;
|
||||
item.nextReview = new Date();
|
||||
|
||||
await item.save();
|
||||
return { success: true, item };
|
||||
};
|
||||
|
||||
@@ -34,6 +34,11 @@ export const getUserStats = async (user) => {
|
||||
}).select('srsLevel');
|
||||
const queueCount = queueItems.length;
|
||||
|
||||
const lessonCount = await StudyItem.countDocuments({
|
||||
userId: userId,
|
||||
srsLevel: 0
|
||||
});
|
||||
|
||||
let hasLowerLevels = false;
|
||||
let lowerLevelCount = 0;
|
||||
if (queueCount > 0) {
|
||||
@@ -94,6 +99,7 @@ export const getUserStats = async (user) => {
|
||||
distribution: dist,
|
||||
forecast: forecast,
|
||||
queueLength: queueCount,
|
||||
lessonCount: lessonCount,
|
||||
hasLowerLevels,
|
||||
lowerLevelCount,
|
||||
heatmap: heatmap,
|
||||
|
||||
@@ -3,56 +3,94 @@ import { StudyItem } from '../models/StudyItem.js';
|
||||
|
||||
export const syncWithWaniKani = async (user) => {
|
||||
const apiKey = user.wkApiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('User has no WaniKani API Key');
|
||||
}
|
||||
if (!apiKey) throw new Error('User has no WaniKani API Key');
|
||||
|
||||
console.log(`Starting sync for user: ${user._id}`);
|
||||
|
||||
// 1. Fetch all assigned Kanji Subject IDs
|
||||
let allSubjectIds = [];
|
||||
let nextUrl = 'https://api.wanikani.com/v2/assignments?subject_types=kanji&started=true';
|
||||
|
||||
try {
|
||||
while (nextUrl) {
|
||||
const res = await fetch(nextUrl, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`WaniKani API Error: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const res = await fetch(nextUrl, { headers: { Authorization: `Bearer ${apiKey}` } });
|
||||
if (!res.ok) throw new Error(`WaniKani API Error: ${res.statusText}`);
|
||||
const json = await res.json();
|
||||
allSubjectIds = allSubjectIds.concat(json.data.map(d => d.data.subject_id));
|
||||
nextUrl = json.pages.next_url;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching assignments:", err);
|
||||
throw new Error("Failed to connect to WaniKani. Check your internet connection.");
|
||||
throw new Error("Failed to connect to WaniKani.");
|
||||
}
|
||||
|
||||
if (allSubjectIds.length === 0) {
|
||||
return { count: 0, message: "No unlocked kanji found.", settings: user.settings };
|
||||
}
|
||||
if (allSubjectIds.length === 0) return { count: 0, message: "No unlocked kanji found." };
|
||||
|
||||
// 2. Filter out items we already have
|
||||
const existingItems = await StudyItem.find({ userId: user._id }).select('wkSubjectId');
|
||||
const existingIds = new Set(existingItems.map(i => i.wkSubjectId));
|
||||
const newIds = allSubjectIds.filter(id => !existingIds.has(id));
|
||||
|
||||
console.log(`Found ${newIds.length} new items to download.`);
|
||||
console.log(`Found ${newIds.length} new items.`);
|
||||
|
||||
const CHUNK_SIZE = 100;
|
||||
// 3. Process in chunks
|
||||
const CHUNK_SIZE = 50; // Reduced chunk size to accommodate secondary radical fetches
|
||||
for (let i = 0; i < newIds.length; i += CHUNK_SIZE) {
|
||||
const chunk = newIds.slice(i, i + CHUNK_SIZE);
|
||||
|
||||
// A. Fetch Kanji Details
|
||||
const subRes = await fetch(`https://api.wanikani.com/v2/subjects?ids=${chunk.join(',')}`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` }
|
||||
});
|
||||
const subJson = await subRes.json();
|
||||
const kanjiDataList = subJson.data;
|
||||
|
||||
const operations = subJson.data.map(d => {
|
||||
// B. Identify all needed Radicals
|
||||
const radicalIdsToFetch = new Set();
|
||||
kanjiDataList.forEach(k => {
|
||||
if (k.data.component_subject_ids) {
|
||||
k.data.component_subject_ids.forEach(rid => radicalIdsToFetch.add(rid));
|
||||
}
|
||||
});
|
||||
|
||||
// C. Fetch Radical Details (if any)
|
||||
const radicalMap = new Map();
|
||||
if (radicalIdsToFetch.size > 0) {
|
||||
const rIds = Array.from(radicalIdsToFetch);
|
||||
// Fetch radicals in sub-chunks if necessary (max 100 per req)
|
||||
for (let j = 0; j < rIds.length; j += 100) {
|
||||
const rChunk = rIds.slice(j, j + 100);
|
||||
const rRes = await fetch(`https://api.wanikani.com/v2/subjects?ids=${rChunk.join(',')}`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` }
|
||||
});
|
||||
const rJson = await rRes.json();
|
||||
|
||||
rJson.data.forEach(r => {
|
||||
const primaryMeaning = r.data.meanings.find(m => m.primary)?.meaning || 'Unknown';
|
||||
const char = r.data.characters;
|
||||
// Find SVG image if no character exists
|
||||
const image = !char && r.data.character_images
|
||||
? r.data.character_images.find(img => img.content_type === 'image/svg+xml' && !img.metadata.inline_styles)?.url
|
||||
: null;
|
||||
|
||||
radicalMap.set(r.id, {
|
||||
meaning: primaryMeaning,
|
||||
char: char,
|
||||
image: image
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// D. Build Database Objects
|
||||
const operations = kanjiDataList.map(d => {
|
||||
const readings = d.data.readings || [];
|
||||
|
||||
// Map the IDs to our fetched radical data
|
||||
const itemRadicals = (d.data.component_subject_ids || [])
|
||||
.map(rid => radicalMap.get(rid))
|
||||
.filter(Boolean); // Remove any not found
|
||||
|
||||
return {
|
||||
userId: user._id,
|
||||
wkSubjectId: d.id,
|
||||
@@ -64,6 +102,7 @@ export const syncWithWaniKani = async (user) => {
|
||||
onyomi: readings.filter(r => r.type === 'onyomi').map(r => r.reading),
|
||||
kunyomi: readings.filter(r => r.type === 'kunyomi').map(r => r.reading),
|
||||
nanori: readings.filter(r => r.type === 'nanori').map(r => r.reading),
|
||||
radicals: itemRadicals, // Save them
|
||||
stats: { correct: 0, total: 0 }
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user