Remove obsolete project artifacts
Some checks failed
Verify and publish container / build-and-push (push) Failing after 33s
Some checks failed
Verify and publish container / build-and-push (push) Failing after 33s
This commit is contained in:
parent
5db7fc8afd
commit
e8dffc9692
58 changed files with 174 additions and 7563 deletions
|
|
@ -2,6 +2,18 @@ import { describe, expect, it } from "vitest";
|
|||
import { summarizeActivityBuckets, summarizeActivityRows } from "./activityService";
|
||||
|
||||
describe("summarizeActivityRows", () => {
|
||||
it("summarizes the remaining activity categories without Arcade fields", () => {
|
||||
const now = new Date("2026-08-07T18:00:00.000Z");
|
||||
const summary = summarizeActivityBuckets([
|
||||
{ date: "2026-08-07", type: "FLASHCARD", count: 2 },
|
||||
{ date: "2026-08-07", type: "QUIZ_QUESTION", count: 3 },
|
||||
], now);
|
||||
|
||||
expect(summary).toMatchObject({ flashcards: 2, questions: 3, currentStreak: 0 });
|
||||
expect(summary.days.find((day) => day.date === summary.today)).toMatchObject({ flashcards: 2, questions: 3, total: 5 });
|
||||
expect(summary).not.toHaveProperty("arcade");
|
||||
});
|
||||
|
||||
it("keeps a 53-week display while preserving a longer current streak", () => {
|
||||
const now = new Date("2026-08-07T18:00:00.000Z");
|
||||
const activities = Array.from({ length: 400 }, (_, age) =>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export interface DailyActivity {
|
|||
date: string;
|
||||
flashcards: number;
|
||||
questions: number;
|
||||
arcade: number;
|
||||
total: number;
|
||||
level: 0 | 1 | 2 | 3 | 4;
|
||||
}
|
||||
|
|
@ -21,7 +20,6 @@ export interface ActivitySummary {
|
|||
activeDays: number;
|
||||
flashcards: number;
|
||||
questions: number;
|
||||
arcade: number;
|
||||
currentStreak: number;
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +46,7 @@ function getLevel(total: number): 0 | 1 | 2 | 3 | 4 {
|
|||
}
|
||||
|
||||
function emptyDay(date: string): DailyActivity {
|
||||
return { date, flashcards: 0, questions: 0, arcade: 0, total: 0, level: 0 };
|
||||
return { date, flashcards: 0, questions: 0, total: 0, level: 0 };
|
||||
}
|
||||
|
||||
export async function recordActivity(type: StudyActivityType) {
|
||||
|
|
@ -96,7 +94,6 @@ export function summarizeActivityBuckets(
|
|||
const day = totals.get(date) ?? emptyDay(date);
|
||||
if (activity.type === "FLASHCARD") day.flashcards += activity.count;
|
||||
if (activity.type === "QUIZ_QUESTION") day.questions += activity.count;
|
||||
if (activity.type === "ARCADE_GROUP") day.arcade += activity.count;
|
||||
day.total += activity.count;
|
||||
day.level = getLevel(day.total);
|
||||
totals.set(date, day);
|
||||
|
|
@ -109,7 +106,6 @@ export function summarizeActivityBuckets(
|
|||
const visibleDays = days.filter((day) => day.date <= today && day.date <= endDate);
|
||||
const flashcards = visibleDays.reduce((sum, day) => sum + day.flashcards, 0);
|
||||
const questions = visibleDays.reduce((sum, day) => sum + day.questions, 0);
|
||||
const arcade = visibleDays.reduce((sum, day) => sum + day.arcade, 0);
|
||||
|
||||
let streakDate = (totals.get(today)?.total ?? 0) >= STREAK_THRESHOLD
|
||||
? today
|
||||
|
|
@ -126,7 +122,6 @@ export function summarizeActivityBuckets(
|
|||
activeDays: visibleDays.filter((day) => day.total > 0).length,
|
||||
flashcards,
|
||||
questions,
|
||||
arcade,
|
||||
currentStreak,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,228 +0,0 @@
|
|||
import { prisma } from "@/lib/db";
|
||||
import { parseConnectionsImportBatch } from "@/lib/arcade/connectionsImport";
|
||||
import { replayConnectionsAttempt } from "@/lib/arcade/connectionsEngine";
|
||||
import { parseCrosswordImportBatch } from "@/lib/arcade/crosswordImport";
|
||||
import { replayCrosswordAttempt } from "@/lib/arcade/crosswordAttempt";
|
||||
import type { ArcadeAttemptCreateInput, CrosswordAttemptCreateInput } from "@/lib/validation/arcadeSchemas";
|
||||
import type {
|
||||
ArcadeAttemptSummary,
|
||||
ArcadeGameKey,
|
||||
ArcadePackSummary,
|
||||
NormalizedArcadePack,
|
||||
NormalizedConnectionsPack,
|
||||
NormalizedCrosswordPack,
|
||||
} from "@/types/arcade";
|
||||
|
||||
function attemptSummary(attempt: {
|
||||
id: string;
|
||||
score: number;
|
||||
maxScore: number;
|
||||
accuracy: number;
|
||||
durationSeconds: number;
|
||||
mistakes: number;
|
||||
completedAt: Date;
|
||||
resultsJson?: string;
|
||||
}): ArcadeAttemptSummary {
|
||||
return {
|
||||
id: attempt.id,
|
||||
score: attempt.score,
|
||||
maxScore: attempt.maxScore,
|
||||
accuracy: attempt.accuracy,
|
||||
durationSeconds: attempt.durationSeconds,
|
||||
mistakes: attempt.mistakes,
|
||||
completedAt: attempt.completedAt.toISOString(),
|
||||
...(attempt.resultsJson ? { results: JSON.parse(attempt.resultsJson) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseImport(gameType: ArcadeGameKey, rawJson: string) {
|
||||
return gameType === "connections" ? parseConnectionsImportBatch(rawJson) : parseCrosswordImportBatch(rawJson);
|
||||
}
|
||||
|
||||
export function previewArcadeImport(gameType: ArcadeGameKey, rawJson: string) {
|
||||
return parseImport(gameType, rawJson);
|
||||
}
|
||||
|
||||
export async function createArcadePacks(input: {
|
||||
classId: string;
|
||||
gameType: ArcadeGameKey;
|
||||
rawJson: string;
|
||||
name?: string;
|
||||
names?: string[];
|
||||
warningsAcknowledged: boolean;
|
||||
}) {
|
||||
const classItem = await prisma.class.findUnique({ where: { id: input.classId }, select: { id: true } });
|
||||
if (!classItem) throw new Error("Class not found");
|
||||
|
||||
const batch = parseImport(input.gameType, input.rawJson);
|
||||
if (input.names && input.names.length !== batch.count) {
|
||||
throw new Error("A name is required for every imported pack.");
|
||||
}
|
||||
if (batch.packs.some((preview) => preview.warnings.length > 0) && !input.warningsAcknowledged) {
|
||||
throw new Error("Import warnings must be acknowledged before saving.");
|
||||
}
|
||||
const maxOrder = await prisma.arcadePack.aggregate({
|
||||
where: { classId: input.classId, gameType: input.gameType },
|
||||
_max: { sortOrder: true },
|
||||
});
|
||||
return prisma.$transaction(async (transaction) => {
|
||||
const created = [];
|
||||
for (const [index, preview] of batch.packs.entries()) {
|
||||
created.push(await transaction.arcadePack.create({
|
||||
data: {
|
||||
classId: input.classId,
|
||||
gameType: input.gameType,
|
||||
name: input.names?.[index]?.trim() || (batch.count === 1 ? input.name?.trim() : undefined) || preview.name,
|
||||
description: preview.description,
|
||||
schemaVersion: preview.normalized.schemaVersion,
|
||||
sourceJson: JSON.stringify(preview.source),
|
||||
normalizedJson: JSON.stringify(preview.normalized),
|
||||
validationReportJson: JSON.stringify({ wasRepaired: preview.wasRepaired, warnings: preview.warnings }),
|
||||
sortOrder: (maxOrder._max.sortOrder ?? -1) + index + 1,
|
||||
},
|
||||
}));
|
||||
}
|
||||
return created;
|
||||
});
|
||||
}
|
||||
|
||||
export async function listArcadePacks(classId: string, gameType: ArcadeGameKey): Promise<ArcadePackSummary[]> {
|
||||
const packs = await prisma.arcadePack.findMany({
|
||||
where: { classId, gameType },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: { attempts: { orderBy: { completedAt: "desc" } } },
|
||||
});
|
||||
return packs.map((pack) => {
|
||||
const normalized = JSON.parse(pack.normalizedJson) as NormalizedArcadePack;
|
||||
const isConnections = normalized.type === "connections";
|
||||
return {
|
||||
id: pack.id,
|
||||
classId: pack.classId,
|
||||
gameType: pack.gameType as ArcadeGameKey,
|
||||
name: pack.name,
|
||||
description: pack.description,
|
||||
schemaVersion: pack.schemaVersion,
|
||||
itemCount: isConnections ? normalized.content.length * 4 : normalized.content.length,
|
||||
defaultAllowedMistakes: isConnections ? normalized.settings.allowedMistakes : 4,
|
||||
defaultInstantCheck: isConnections ? false : normalized.settings.allowInstantCheck,
|
||||
defaultAllowHints: isConnections ? true : normalized.settings.allowHints,
|
||||
bestScore: pack.attempts.length ? Math.max(...pack.attempts.map((attempt) => attempt.score)) : null,
|
||||
latestAttempt: pack.attempts[0] ? attemptSummary(pack.attempts[0]) : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getArcadePack(id: string) {
|
||||
const pack = await prisma.arcadePack.findUnique({
|
||||
where: { id },
|
||||
include: { class: { select: { slug: true, name: true } } },
|
||||
});
|
||||
if (!pack) return null;
|
||||
return {
|
||||
...pack,
|
||||
gameType: pack.gameType as ArcadeGameKey,
|
||||
normalized: JSON.parse(pack.normalizedJson) as NormalizedArcadePack,
|
||||
validationReport: JSON.parse(pack.validationReportJson),
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateArcadePack(id: string, name: string) {
|
||||
const existing = await prisma.arcadePack.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!existing) return null;
|
||||
return prisma.arcadePack.update({ where: { id }, data: { name } });
|
||||
}
|
||||
|
||||
export async function deleteArcadePack(id: string) {
|
||||
const existing = await prisma.arcadePack.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!existing) return false;
|
||||
await prisma.arcadePack.delete({ where: { id } });
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function listArcadeAttempts(arcadePackId: string) {
|
||||
const attempts = await prisma.arcadeAttempt.findMany({
|
||||
where: { arcadePackId },
|
||||
orderBy: { completedAt: "desc" },
|
||||
take: 20,
|
||||
});
|
||||
return attempts.map(attemptSummary);
|
||||
}
|
||||
|
||||
export async function createArcadeAttempt(arcadePackId: string, input: ArcadeAttemptCreateInput) {
|
||||
const pack = await prisma.arcadePack.findUnique({ where: { id: arcadePackId } });
|
||||
if (!pack) return null;
|
||||
const normalized = JSON.parse(pack.normalizedJson) as NormalizedConnectionsPack;
|
||||
if (normalized.type !== "connections") throw new Error("This attempt does not match the pack game type.");
|
||||
const replay = replayConnectionsAttempt(
|
||||
normalized,
|
||||
input.submissions,
|
||||
input.settings,
|
||||
input.durationSeconds
|
||||
);
|
||||
if (!replay.complete) throw new Error("Only completed Connections rounds can be saved.");
|
||||
|
||||
const solvedGroups = replay.result.groups.filter((group) => group.solved).length;
|
||||
return prisma.$transaction(async (transaction) => {
|
||||
const attempt = await transaction.arcadeAttempt.create({
|
||||
data: {
|
||||
arcadePackId,
|
||||
mode: input.mode,
|
||||
score: replay.result.score,
|
||||
maxScore: replay.result.maxScore,
|
||||
accuracy: replay.result.accuracy,
|
||||
durationSeconds: replay.result.durationSeconds,
|
||||
mistakes: replay.result.mistakes,
|
||||
hintsUsed: replay.result.hintsUsed,
|
||||
settingsJson: JSON.stringify(input.settings),
|
||||
resultsJson: JSON.stringify(replay.result),
|
||||
seed: input.seed,
|
||||
},
|
||||
});
|
||||
if (solvedGroups > 0) {
|
||||
await transaction.studyActivity.createMany({
|
||||
data: Array.from({ length: solvedGroups }, () => ({ type: "ARCADE_GROUP" })),
|
||||
});
|
||||
}
|
||||
return attemptSummary({ ...attempt, resultsJson: attempt.resultsJson });
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCrosswordAttempt(arcadePackId: string, input: CrosswordAttemptCreateInput) {
|
||||
const pack = await prisma.arcadePack.findUnique({ where: { id: arcadePackId } });
|
||||
if (!pack) return null;
|
||||
const normalized = JSON.parse(pack.normalizedJson) as NormalizedCrosswordPack;
|
||||
if (normalized.type !== "crossword") throw new Error("This attempt does not match the pack game type.");
|
||||
const result = replayCrosswordAttempt(
|
||||
normalized,
|
||||
input.seed,
|
||||
input.settings,
|
||||
input.finalAnswers,
|
||||
input.actions,
|
||||
input.durationSeconds,
|
||||
input.gaveUp
|
||||
);
|
||||
const correctWords = result.entries.filter((entry) => !entry.omitted && entry.correct && !entry.revealedWord).length;
|
||||
return prisma.$transaction(async (transaction) => {
|
||||
const attempt = await transaction.arcadeAttempt.create({
|
||||
data: {
|
||||
arcadePackId,
|
||||
mode: input.mode,
|
||||
score: result.score,
|
||||
maxScore: result.maxScore,
|
||||
accuracy: result.accuracy,
|
||||
durationSeconds: result.durationSeconds,
|
||||
mistakes: result.mistakes,
|
||||
hintsUsed: result.hintsUsed,
|
||||
settingsJson: JSON.stringify(input.settings),
|
||||
resultsJson: JSON.stringify(result),
|
||||
seed: input.seed,
|
||||
},
|
||||
});
|
||||
if (correctWords > 0) {
|
||||
await transaction.studyActivity.createMany({
|
||||
data: Array.from({ length: correctWords }, () => ({ type: "ARCADE_WORD" })),
|
||||
});
|
||||
}
|
||||
return attemptSummary({ ...attempt, resultsJson: attempt.resultsJson });
|
||||
});
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { prisma } from "@/lib/db";
|
||||
import { ARCADE_SERVER_REGISTRY } from "@/lib/arcade/registry";
|
||||
|
||||
export type LlmInstructionType = "flashcards" | "quizzes" | "connections" | "crossword";
|
||||
export type LlmInstructionType = "flashcards" | "quizzes";
|
||||
|
||||
const DEFAULT_LLM_INSTRUCTIONS_FLASHCARDS = `You are generating study materials in a strict JSON format for import into a
|
||||
personal study app. The overall output must be raw JSON with no wrapping
|
||||
|
|
@ -53,9 +52,7 @@ Material:
|
|||
|
||||
function instructionConfig(type: LlmInstructionType) {
|
||||
if (type === "flashcards") return { key: "llmInstructionsFlashcards", value: DEFAULT_LLM_INSTRUCTIONS_FLASHCARDS };
|
||||
if (type === "quizzes") return { key: "llmInstructionsQuizzes", value: DEFAULT_LLM_INSTRUCTIONS_QUIZZES };
|
||||
if (type === "connections") return { key: "llmInstructionsConnections", value: ARCADE_SERVER_REGISTRY.connections.defaultInstructions };
|
||||
return { key: "llmInstructionsCrossword", value: ARCADE_SERVER_REGISTRY.crossword.defaultInstructions };
|
||||
return { key: "llmInstructionsQuizzes", value: DEFAULT_LLM_INSTRUCTIONS_QUIZZES };
|
||||
}
|
||||
|
||||
export async function getLlmInstructions(type: LlmInstructionType): Promise<string> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue