From 02e7f927a460a47dd81171471c0edd885673527f Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:01:08 +0100
Subject: [PATCH 01/10] Fix daily rewards practice and lab CTA navigation
---
components/dailyRewards/Card.vue | 336 +++++++++++++++++++++++++++++++
1 file changed, 336 insertions(+)
create mode 100644 components/dailyRewards/Card.vue
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
new file mode 100644
index 00000000..2ce6f768
--- /dev/null
+++ b/components/dailyRewards/Card.vue
@@ -0,0 +1,336 @@
+
+
+
+
+
+ {{ t("DailyRewards.Heading") }}
+
+
+ +{{ availableCoins }} {{ t("DailyRewards.Coins") }}
+
+
+
+ {{ t("DailyRewards.Subheading") }}
+
+
+ {{ t("DailyRewards.Countdown", { relative: countdownLabel }) }}
+
+
+
+
+
+
+
{{ t("DailyRewards.Error") }}
+
+
+
+
+
{{ t("DailyRewards.Disabled") }}
+
+
+
+
+
+
+
+ {{ t("DailyRewards.Footer", { coins: claimedToday }) }}
+
+
+
+
+
+
+ {{ ariaAnnouncement || "" }}
+
+
+
+
+
+
+
From 6fc0b8b494eb63254e09bb5e5275c725350aafd8 Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:02:59 +0100
Subject: [PATCH 02/10] Daily Rewards
---
components/dailyRewards/CategoryRow.vue | 259 ++++++++++++++++++
composables/dailyRewards.ts | 340 ++++++++++++++++++++++++
locales/de.json | 59 ++++
locales/en-US.json | 59 ++++
pages/dashboard/index.vue | 4 +
5 files changed, 721 insertions(+)
create mode 100644 components/dailyRewards/CategoryRow.vue
create mode 100644 composables/dailyRewards.ts
diff --git a/components/dailyRewards/CategoryRow.vue b/components/dailyRewards/CategoryRow.vue
new file mode 100644
index 00000000..2445cd0d
--- /dev/null
+++ b/components/dailyRewards/CategoryRow.vue
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+ {{ categoryLabel }}
+
+
+
+ {{ statusLabel }}
+
+
+ +{{ reward.coins }} {{ t("DailyRewards.Coins") }}
+
+
+
+
+ {{ activityHint }}
+
+
+ {{ defaultHint }}
+
+
+
+
+
+
+
+
+
+
+ {{ defaultHint }}
+
+
+
+ {{ unavailableLabel }}
+
+
+
+ {{ claimedLabel }}
+
+
+
+
+
+
+
+
diff --git a/composables/dailyRewards.ts b/composables/dailyRewards.ts
new file mode 100644
index 00000000..8316700e
--- /dev/null
+++ b/composables/dailyRewards.ts
@@ -0,0 +1,340 @@
+import { refreshNuxtData, useAsyncData, useState } from "#app";
+import { useIntervalFn, useEventListener, useNow } from "@vueuse/core";
+import { computed, onBeforeUnmount, ref, watch } from "vue";
+import { GET, POST } from "./fetch";
+
+const DAILY_REWARDS_KEY = "daily-rewards";
+const TEN_MINUTES_MS = 10 * 60 * 1000;
+
+export type RewardCategory = "arrival" | "lecture" | "practice" | "lab";
+export type RewardStatus = "pending" | "ready" | "claimed" | "unavailable";
+
+export interface DailyReward {
+ category: RewardCategory;
+ coins: number;
+ status: RewardStatus;
+ claimable_since: string | null;
+ last_detected_at: string | null;
+ claimed_at: string | null;
+ activity_sample?: Record | null;
+ unavailable_reason?: string | null;
+}
+
+export interface DailyRewardsPayload {
+ date: string;
+ feature_enabled: boolean;
+ rewards: DailyReward[];
+ claim_totals?: {
+ available: number;
+ claimed_today: number;
+ };
+}
+
+export interface ClaimAllResponse {
+ status: "ok";
+ claimed_categories: RewardCategory[];
+ skipped_categories: { category: RewardCategory; reason: string }[];
+}
+
+interface ClaimOutcome {
+ ok: boolean;
+ category: RewardCategory;
+ error?: string;
+}
+
+interface ClaimAllOutcome {
+ ok: boolean;
+ claimed: RewardCategory[];
+ skipped: { category: RewardCategory; reason: string }[];
+ error?: string;
+}
+
+const categories: RewardCategory[] = ["arrival", "lecture", "practice", "lab"];
+
+export function useDailyRewards() {
+ const ariaAnnouncement = useState(
+ "daily-rewards-aria-announcement",
+ () => null,
+ );
+
+ const claimBusy = ref>(
+ categories.reduce(
+ (acc, category) => {
+ acc[category] = false;
+ return acc;
+ },
+ {} as Record,
+ ),
+ );
+
+ const claimAllBusy = ref(false);
+
+ const { data, pending, error, refresh } = useAsyncData(
+ DAILY_REWARDS_KEY,
+ async () => {
+ const response = await GET("/daily-rewards");
+ return response as DailyRewardsPayload;
+ },
+ {
+ server: true,
+ watch: [],
+ },
+ );
+
+ const rewards = computed(() => data.value?.rewards ?? []);
+
+ const availableCoins = computed(() =>
+ rewards.value
+ .filter((reward) => reward.status === "ready")
+ .reduce((total, reward) => total + (reward.coins ?? 0), 0),
+ );
+
+ const claimedToday = computed(() => data.value?.claim_totals?.claimed_today ?? 0);
+
+ const isClient = typeof window !== "undefined";
+ const currentTime = isClient ? useNow({ interval: 60_000 }) : ref(new Date());
+
+ const needsResetRefresh = computed(() => {
+ if (!data.value?.date) return true;
+ const dayStart = new Date(`${data.value.date}T00:00:00Z`);
+ if (Number.isNaN(dayStart.getTime())) return true;
+ const nextDay = new Date(dayStart);
+ nextDay.setUTCDate(nextDay.getUTCDate() + 1);
+ return currentTime.value.getTime() >= nextDay.getTime();
+ });
+
+ const shouldPoll = computed(
+ () =>
+ needsResetRefresh.value ||
+ rewards.value.some((reward) => reward.status !== "claimed"),
+ );
+
+ const countdownLabel = computed(() =>
+ formatResetCountdown(data.value?.date, currentTime.value),
+ );
+
+ const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
+ () => refreshNuxtData(DAILY_REWARDS_KEY),
+ TEN_MINUTES_MS,
+ {
+ immediate: false,
+ immediateCallback: false,
+ },
+ );
+
+ if (!isClient) {
+ pausePolling();
+ }
+
+ if (isClient) {
+ watch(
+ shouldPoll,
+ (active) => {
+ if (active && !document.hidden) {
+ resumePolling();
+ } else {
+ pausePolling();
+ }
+ },
+ { immediate: true },
+ );
+
+ useEventListener(
+ () => document,
+ "visibilitychange",
+ () => {
+ if (document.hidden) {
+ pausePolling();
+ return;
+ }
+ if (shouldPoll.value) {
+ resumePolling();
+ }
+ },
+ );
+ }
+
+ onBeforeUnmount(() => {
+ if (isClient) {
+ pausePolling();
+ }
+ });
+
+ const mutateReward = (
+ category: RewardCategory,
+ updater: (reward: DailyReward) => DailyReward,
+ ) => {
+ if (!data.value) return;
+ data.value = {
+ ...data.value,
+ rewards: data.value.rewards.map((reward) =>
+ reward.category === category ? updater({ ...reward }) : reward,
+ ),
+ };
+ };
+
+ const resetAnnouncement = () => {
+ ariaAnnouncement.value = "";
+ };
+
+ const claimCategory = async (category: RewardCategory): Promise => {
+ if (claimBusy.value[category]) {
+ return { ok: false, category, error: "busy" };
+ }
+
+ claimBusy.value = { ...claimBusy.value, [category]: true };
+
+ const originalReward = rewards.value.find((reward) => reward.category === category);
+ const optimisticTimestamp = new Date().toISOString();
+
+ if (originalReward && originalReward.status === "ready") {
+ mutateReward(category, (reward) => ({
+ ...reward,
+ status: "claimed",
+ claimed_at: optimisticTimestamp,
+ }));
+ }
+
+ try {
+ await POST(`/daily-rewards/${category}/claim`);
+ resetAnnouncement();
+ ariaAnnouncement.value = buildClaimAnnouncement(category, originalReward?.coins ?? 0);
+ await refreshNuxtData(DAILY_REWARDS_KEY);
+ return { ok: true, category };
+ } catch (err: any) {
+ if (originalReward) {
+ mutateReward(category, () => ({ ...originalReward }));
+ }
+ return {
+ ok: false,
+ category,
+ error: normalizeError(err),
+ };
+ } finally {
+ claimBusy.value = { ...claimBusy.value, [category]: false };
+ }
+ };
+
+ const claimAll = async (): Promise => {
+ if (claimAllBusy.value) {
+ return { ok: false, claimed: [], skipped: [], error: "busy" };
+ }
+
+ claimAllBusy.value = true;
+ const readyRewards = rewards.value.filter((reward) => reward.status === "ready");
+ const optimisticTimestamp = new Date().toISOString();
+ const snapshot = data.value ? structuredClone(data.value) : null;
+
+ if (readyRewards.length > 0) {
+ data.value = {
+ ...(data.value as DailyRewardsPayload),
+ rewards: rewards.value.map((reward) =>
+ reward.status === "ready"
+ ? { ...reward, status: "claimed", claimed_at: optimisticTimestamp }
+ : reward,
+ ),
+ };
+ }
+
+ try {
+ const response = (await POST("/daily-rewards/claim-all")) as ClaimAllResponse;
+ if (response?.claimed_categories?.length) {
+ const totalCoins = readyRewards
+ .filter((reward) => response.claimed_categories.includes(reward.category))
+ .reduce((sum, reward) => sum + reward.coins, 0);
+ resetAnnouncement();
+ ariaAnnouncement.value = buildClaimAllAnnouncement(
+ response.claimed_categories,
+ totalCoins,
+ );
+ }
+ await refreshNuxtData(DAILY_REWARDS_KEY);
+ return {
+ ok: true,
+ claimed: response?.claimed_categories ?? [],
+ skipped: response?.skipped_categories ?? [],
+ };
+ } catch (err: any) {
+ if (snapshot) {
+ data.value = snapshot;
+ }
+ return {
+ ok: false,
+ claimed: [],
+ skipped: [],
+ error: normalizeError(err),
+ };
+ } finally {
+ claimAllBusy.value = false;
+ }
+ };
+
+ return {
+ data,
+ rewards,
+ availableCoins,
+ claimedToday,
+ countdownLabel,
+ featureEnabled: computed(() => !!data.value?.feature_enabled),
+ pending,
+ error,
+ refresh: () => refreshNuxtData(DAILY_REWARDS_KEY),
+ claimCategory,
+ claimAll,
+ claimBusy,
+ claimAllBusy,
+ ariaAnnouncement,
+ resetAnnouncement,
+ };
+}
+
+function formatResetCountdown(dateIso?: string | null, nowReference?: Date) {
+ if (!dateIso) return "";
+ const start = new Date(`${dateIso}T00:00:00Z`);
+ if (Number.isNaN(start.getTime())) return "";
+
+ const nextDay = new Date(start);
+ nextDay.setUTCDate(nextDay.getUTCDate() + 1);
+
+ const reference = nowReference ?? new Date();
+ const diffMs = nextDay.getTime() - reference.getTime();
+ if (diffMs <= 0) return "";
+
+ const diffMinutes = Math.round(diffMs / (1000 * 60));
+
+ if (diffMinutes >= 60) {
+ const hours = Math.round(diffMinutes / 60);
+ return new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(
+ hours,
+ "hour",
+ );
+ }
+
+ return new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(
+ diffMinutes || 1,
+ "minute",
+ );
+}
+
+function buildClaimAnnouncement(category: RewardCategory, coins: number) {
+ return `${capitalize(category)} reward claimed for ${coins} coins.`;
+}
+
+function buildClaimAllAnnouncement(categories: RewardCategory[], coins: number) {
+ if (!categories.length) return "";
+ return `Claimed ${categories.length} rewards for ${coins} coins.`;
+}
+
+function capitalize(value: string) {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
+
+function normalizeError(error: any) {
+ if (!error) return "unknown";
+ const detail =
+ error?.data?.detail ??
+ error?.response?._data?.detail ??
+ error?.message ??
+ error?.toString();
+ return typeof detail === "string" ? detail : "unknown";
+}
diff --git a/locales/de.json b/locales/de.json
index 49d569cb..bbdb5542 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -387,6 +387,65 @@
"MyAccount": "Mein Konto",
"SelectSkill": "Skill auswählen"
},
+ "DailyRewards": {
+ "Heading": "T\u00e4gliche Belohnungen",
+ "Subheading": "Melde dich an, bleib dran und sammle deine Coins ein.",
+ "Coins": "Coins",
+ "Countdown": "Reset {relative}",
+ "Error": "Belohnungen konnten nicht geladen werden. Versuch es gleich nochmal.",
+ "Disabled": "Daily Rewards sind noch nicht aktiv.",
+ "Footer": "Heute bereits eingesackt: {coins} Coins",
+ "Buttons": {
+ "Claim": "Einsacken",
+ "ClaimAll": "Alles einsacken",
+ "Retry": "Nochmal versuchen",
+ "Resume": {
+ "arrival": "Check-in starten",
+ "lecture": "Vorlesung fortsetzen",
+ "practice": "Jetzt ueben",
+ "lab": "Lab starten"
+ }
+ },
+ "Categories": {
+ "arrival": "Daily Check-in",
+ "lecture": "Lecture Run",
+ "practice": "Practice Grind",
+ "lab": "Lab Breakthrough"
+ },
+ "Status": {
+ "pending": "Ausstehend",
+ "ready": "Bereit",
+ "claimed": "Eingeloest",
+ "unavailable": "Nicht verfuegbar"
+ },
+ "StatusLabel": {
+ "ClaimedAt": "Eingeloest um {time}"
+ },
+ "Unavailable": {
+ "no_recommendation": "Noch keine Empfehlung.",
+ "default": "Gerade nicht verfuegbar."
+ },
+ "DefaultHint": {
+ "arrival": "Hol dir den taeglichen Check-in fuer direkte Coins.",
+ "lecture": "Schliesse heute eine Vorlesung ab, um das zu triggern.",
+ "practice": "Quiz oder Matching loesen und die Leiste fuellen.",
+ "lab": "Ein Lab abschliessen, dann blinkt die Belohnung."
+ },
+ "Toast": {
+ "ClaimSuccess": "Belohnung gesichert!",
+ "ClaimError": "Claim fehlgeschlagen",
+ "ClaimAllSuccess": "Loot eingesackt!",
+ "ClaimAllPartial": "Teilweise eingesackt",
+ "ClaimAllError": "Claim-all fehlgeschlagen"
+ },
+ "ToastBody": {
+ "ClaimSingle": "{category} hat dir {coins} Coins eingebracht.",
+ "ClaimError": "{category} konnte nicht eingesackt werden. Versuch es erneut.",
+ "ClaimAllSuccess": "Du hast {coins} Coins eingesackt.",
+ "ClaimAllPartial": "Noch offen: {categories}. Schnapp sie dir!",
+ "ClaimAllError": "Neu laden oder spaeter wieder probieren."
+ }
+ },
"Body": {
"DashboardIntro": "Steig direkt wieder in deine Kurse oder Challenges ein.",
"SkillTreeOverview": "Behalte deine Root-Skills im Blick und öffne die Details, wenn du tiefer einsteigen möchtest.",
diff --git a/locales/en-US.json b/locales/en-US.json
index d0db6ef0..c6e7b46e 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -388,6 +388,65 @@
"MyAccount": "My Account",
"SelectSkill": "Select Skill"
},
+ "DailyRewards": {
+ "Heading": "Daily Rewards",
+ "Subheading": "Sign in, keep learning, and collect your coins each day.",
+ "Coins": "coins",
+ "Countdown": "Resets {relative}",
+ "Error": "We couldn't load your rewards. Try again in a moment.",
+ "Disabled": "Daily rewards are warming up and will unlock soon.",
+ "Footer": "Coins claimed today: {coins}",
+ "Buttons": {
+ "Claim": "Claim",
+ "ClaimAll": "Claim all",
+ "Retry": "Try again",
+ "Resume": {
+ "arrival": "Check in",
+ "lecture": "Jump into a lecture",
+ "practice": "Practice now",
+ "lab": "Launch a lab"
+ }
+ },
+ "Categories": {
+ "arrival": "Daily Check-in",
+ "lecture": "Lecture Run",
+ "practice": "Practice Grind",
+ "lab": "Lab Breakthrough"
+ },
+ "Status": {
+ "pending": "Pending",
+ "ready": "Ready",
+ "claimed": "Claimed",
+ "unavailable": "Unavailable"
+ },
+ "StatusLabel": {
+ "ClaimedAt": "Claimed at {time}"
+ },
+ "Unavailable": {
+ "no_recommendation": "No fresh recommendation yet.",
+ "default": "Not available right now."
+ },
+ "DefaultHint": {
+ "arrival": "Claim your daily check-in for instant coins.",
+ "lecture": "Finish a lecture today to light this up.",
+ "practice": "Clear a quiz or matching task to charge the meter.",
+ "lab": "Ship a lab solution to trigger this reward."
+ },
+ "Toast": {
+ "ClaimSuccess": "Reward secured!",
+ "ClaimError": "Claim failed",
+ "ClaimAllSuccess": "Loot haul!",
+ "ClaimAllPartial": "Partial haul",
+ "ClaimAllError": "Claim all failed"
+ },
+ "ToastBody": {
+ "ClaimSingle": "{category} dropped {coins} coins into your bank.",
+ "ClaimError": "Couldn't claim {category}. Try again.",
+ "ClaimAllSuccess": "You grabbed {coins} coins.",
+ "ClaimAllPartial": "Still pending: {categories}. Knock those out!",
+ "ClaimAllError": "Reload or try again in a bit."
+ }
+ },
"Body": {
"DashboardIntro": "Jump back into your courses or challenges to keep the momentum going.",
"SkillTreeOverview": "Browse every root skill and open the details whenever you want to go deeper.",
diff --git a/pages/dashboard/index.vue b/pages/dashboard/index.vue
index 2911113b..2c15c529 100644
--- a/pages/dashboard/index.vue
+++ b/pages/dashboard/index.vue
@@ -9,6 +9,10 @@
+
+
From facfc601d1a7fe42fcbc1daface696b243d03034 Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:08:13 +0100
Subject: [PATCH 03/10] style: fix daily rewards formatting
---
components/dailyRewards/Card.vue | 50 +++++++++++++++----------
components/dailyRewards/CategoryRow.vue | 32 +++++++---------
composables/dailyRewards.ts | 50 +++++++++----------------
3 files changed, 62 insertions(+), 70 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index 2ce6f768..1f86a8e8 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -1,6 +1,6 @@
-
+
-
+
{{ t("DailyRewards.Error") }}
-
+
{{ t("DailyRewards.Disabled") }}
@@ -113,14 +123,14 @@ export default defineComponent({
} = useDailyRewards();
const hasReadyRewards = computed(() =>
- rewards.value.some((reward) => reward.status === "ready"),
+ rewards.value.some((reward) => reward.status === "ready")
);
const summaryByCategory = computed(() =>
rewards.value.reduce
>((acc, reward) => {
acc[reward.category] = reward;
return acc;
- }, Object.create(null)),
+ }, Object.create(null))
);
const handleClaim = async (category: RewardCategory) => {
@@ -133,7 +143,7 @@ export default defineComponent({
t("DailyRewards.ToastBody.ClaimSingle", {
category: t(`DailyRewards.Categories.${category}`),
coins: current?.coins ?? 0,
- }),
+ })
);
} else if (claimError !== "busy") {
openSnackbar(
@@ -141,23 +151,26 @@ export default defineComponent({
"DailyRewards.Toast.ClaimError",
t("DailyRewards.ToastBody.ClaimError", {
category: t(`DailyRewards.Categories.${category}`),
- }),
+ })
);
}
};
const handleClaimAll = async () => {
const readyRewards = rewards.value.filter((reward) => reward.status === "ready");
- const readyCoinMap = readyRewards.reduce>((acc, reward) => {
- acc[reward.category] = reward.coins;
- return acc;
- }, {} as Record);
+ const readyCoinMap = readyRewards.reduce>(
+ (acc, reward) => {
+ acc[reward.category] = reward.coins;
+ return acc;
+ },
+ {} as Record
+ );
const result = await claimAll();
if (result.ok) {
const claimedCoins = result.claimed.reduce(
(sum, category) => sum + (readyCoinMap[category] ?? 0),
- 0,
+ 0
);
const successBody = t("DailyRewards.ToastBody.ClaimAllSuccess", {
coins: claimedCoins,
@@ -171,13 +184,13 @@ export default defineComponent({
.map(({ category }) => t(`DailyRewards.Categories.${category}`))
.join(", "),
})}`
- : successBody,
+ : successBody
);
} else if (result.error !== "busy") {
openSnackbar(
"error",
"DailyRewards.Toast.ClaimAllError",
- t("DailyRewards.ToastBody.ClaimAllError"),
+ t("DailyRewards.ToastBody.ClaimAllError")
);
}
};
@@ -219,8 +232,7 @@ export default defineComponent({
(pickSampleValue("skill_id", "skillId") ? "skill" : undefined) ??
(pickSampleValue("quiz_id", "quizId") ? "quiz" : undefined);
let solveId =
- pickSampleValue("solve_id", "solveId") ??
- pickSampleValue("quiz_id", "quizId");
+ pickSampleValue("solve_id", "solveId") ?? pickSampleValue("quiz_id", "quizId");
if (!solveId) {
if (resolvedQuizzesFrom === "course") {
@@ -236,7 +248,7 @@ export default defineComponent({
"query_subtask_id",
"querySubTaskId",
"subtask_id",
- "subTaskId",
+ "subTaskId"
);
const taskId = pickSampleValue("task_id", "taskId");
const rootSkillId = pickSampleValue("root_skill_id", "rootSkillId", "rootSkillID");
@@ -277,7 +289,7 @@ export default defineComponent({
"coding_challenge_id",
"codingChallengeId",
"subtask_id",
- "subTaskId",
+ "subTaskId"
);
if (challengeId) {
if (codingChallengeId) {
diff --git a/components/dailyRewards/CategoryRow.vue b/components/dailyRewards/CategoryRow.vue
index 2445cd0d..2cb4f037 100644
--- a/components/dailyRewards/CategoryRow.vue
+++ b/components/dailyRewards/CategoryRow.vue
@@ -44,11 +44,14 @@
@@ -75,10 +78,7 @@
{{ unavailableLabel }}
-
+
{{ claimedLabel }}
@@ -125,32 +125,28 @@ export default defineComponent({
let celebrationTimeout: ReturnType
| null = null;
const categoryLabel = computed(() =>
- t(`DailyRewards.Categories.${props.reward.category}` as const),
+ t(`DailyRewards.Categories.${props.reward.category}` as const)
);
- const statusLabel = computed(() =>
- t(`DailyRewards.Status.${props.reward.status}` as const),
- );
+ const statusLabel = computed(() => t(`DailyRewards.Status.${props.reward.status}` as const));
const statusBadgeClass = computed(
- () => STATUS_CLASS[props.reward.status] ?? STATUS_CLASS.pending,
+ () => STATUS_CLASS[props.reward.status] ?? STATUS_CLASS.pending
);
- const statusDotClass = computed(
- () => DOT_CLASS[props.reward.status] ?? DOT_CLASS.pending,
- );
+ const statusDotClass = computed(() => DOT_CLASS[props.reward.status] ?? DOT_CLASS.pending);
const activityHint = computed(() => buildActivityHint(props.reward));
const defaultHint = computed(() =>
- t(`DailyRewards.DefaultHint.${props.reward.category}` as const),
+ t(`DailyRewards.DefaultHint.${props.reward.category}` as const)
);
const showPendingCta = computed(
- () => props.reward.status === "pending" && props.reward.category !== "arrival",
+ () => props.reward.status === "pending" && props.reward.category !== "arrival"
);
const pendingCtaLabel = computed(() =>
- t(`DailyRewards.Buttons.Resume.${props.reward.category}` as const),
+ t(`DailyRewards.Buttons.Resume.${props.reward.category}` as const)
);
const unavailableLabel = computed(() => {
@@ -180,7 +176,7 @@ export default defineComponent({
} else if (status !== "claimed") {
celebrate.value = false;
}
- },
+ }
);
onBeforeUnmount(() => {
diff --git a/composables/dailyRewards.ts b/composables/dailyRewards.ts
index 8316700e..23e3a560 100644
--- a/composables/dailyRewards.ts
+++ b/composables/dailyRewards.ts
@@ -52,10 +52,7 @@ interface ClaimAllOutcome {
const categories: RewardCategory[] = ["arrival", "lecture", "practice", "lab"];
export function useDailyRewards() {
- const ariaAnnouncement = useState(
- "daily-rewards-aria-announcement",
- () => null,
- );
+ const ariaAnnouncement = useState("daily-rewards-aria-announcement", () => null);
const claimBusy = ref>(
categories.reduce(
@@ -63,8 +60,8 @@ export function useDailyRewards() {
acc[category] = false;
return acc;
},
- {} as Record,
- ),
+ {} as Record
+ )
);
const claimAllBusy = ref(false);
@@ -78,7 +75,7 @@ export function useDailyRewards() {
{
server: true,
watch: [],
- },
+ }
);
const rewards = computed(() => data.value?.rewards ?? []);
@@ -86,7 +83,7 @@ export function useDailyRewards() {
const availableCoins = computed(() =>
rewards.value
.filter((reward) => reward.status === "ready")
- .reduce((total, reward) => total + (reward.coins ?? 0), 0),
+ .reduce((total, reward) => total + (reward.coins ?? 0), 0)
);
const claimedToday = computed(() => data.value?.claim_totals?.claimed_today ?? 0);
@@ -104,14 +101,10 @@ export function useDailyRewards() {
});
const shouldPoll = computed(
- () =>
- needsResetRefresh.value ||
- rewards.value.some((reward) => reward.status !== "claimed"),
+ () => needsResetRefresh.value || rewards.value.some((reward) => reward.status !== "claimed")
);
- const countdownLabel = computed(() =>
- formatResetCountdown(data.value?.date, currentTime.value),
- );
+ const countdownLabel = computed(() => formatResetCountdown(data.value?.date, currentTime.value));
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
() => refreshNuxtData(DAILY_REWARDS_KEY),
@@ -119,7 +112,7 @@ export function useDailyRewards() {
{
immediate: false,
immediateCallback: false,
- },
+ }
);
if (!isClient) {
@@ -136,7 +129,7 @@ export function useDailyRewards() {
pausePolling();
}
},
- { immediate: true },
+ { immediate: true }
);
useEventListener(
@@ -150,7 +143,7 @@ export function useDailyRewards() {
if (shouldPoll.value) {
resumePolling();
}
- },
+ }
);
}
@@ -162,13 +155,13 @@ export function useDailyRewards() {
const mutateReward = (
category: RewardCategory,
- updater: (reward: DailyReward) => DailyReward,
+ updater: (reward: DailyReward) => DailyReward
) => {
if (!data.value) return;
data.value = {
...data.value,
rewards: data.value.rewards.map((reward) =>
- reward.category === category ? updater({ ...reward }) : reward,
+ reward.category === category ? updater({ ...reward }) : reward
),
};
};
@@ -231,7 +224,7 @@ export function useDailyRewards() {
rewards: rewards.value.map((reward) =>
reward.status === "ready"
? { ...reward, status: "claimed", claimed_at: optimisticTimestamp }
- : reward,
+ : reward
),
};
}
@@ -243,10 +236,7 @@ export function useDailyRewards() {
.filter((reward) => response.claimed_categories.includes(reward.category))
.reduce((sum, reward) => sum + reward.coins, 0);
resetAnnouncement();
- ariaAnnouncement.value = buildClaimAllAnnouncement(
- response.claimed_categories,
- totalCoins,
- );
+ ariaAnnouncement.value = buildClaimAllAnnouncement(response.claimed_categories, totalCoins);
}
await refreshNuxtData(DAILY_REWARDS_KEY);
return {
@@ -304,15 +294,12 @@ function formatResetCountdown(dateIso?: string | null, nowReference?: Date) {
if (diffMinutes >= 60) {
const hours = Math.round(diffMinutes / 60);
- return new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(
- hours,
- "hour",
- );
+ return new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(hours, "hour");
}
return new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }).format(
diffMinutes || 1,
- "minute",
+ "minute"
);
}
@@ -332,9 +319,6 @@ function capitalize(value: string) {
function normalizeError(error: any) {
if (!error) return "unknown";
const detail =
- error?.data?.detail ??
- error?.response?._data?.detail ??
- error?.message ??
- error?.toString();
+ error?.data?.detail ?? error?.response?._data?.detail ?? error?.message ?? error?.toString();
return typeof detail === "string" ? detail : "unknown";
}
From 1f988c4fc952dee1c29510a4f86bbcadd734c34e Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:32:13 +0100
Subject: [PATCH 04/10] Fix daily rewards snapshot and claim-all parsing
---
components/dailyRewards/Card.vue | 15 +------------
composables/dailyRewards.ts | 37 +++++++++++++++++++++-----------
2 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index 1f86a8e8..8c7599db 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -157,23 +157,10 @@ export default defineComponent({
};
const handleClaimAll = async () => {
- const readyRewards = rewards.value.filter((reward) => reward.status === "ready");
- const readyCoinMap = readyRewards.reduce>(
- (acc, reward) => {
- acc[reward.category] = reward.coins;
- return acc;
- },
- {} as Record
- );
-
const result = await claimAll();
if (result.ok) {
- const claimedCoins = result.claimed.reduce(
- (sum, category) => sum + (readyCoinMap[category] ?? 0),
- 0
- );
const successBody = t("DailyRewards.ToastBody.ClaimAllSuccess", {
- coins: claimedCoins,
+ coins: result.totalClaimedCoins,
});
openSnackbar(
"success",
diff --git a/composables/dailyRewards.ts b/composables/dailyRewards.ts
index 23e3a560..2f64cedf 100644
--- a/composables/dailyRewards.ts
+++ b/composables/dailyRewards.ts
@@ -21,7 +21,7 @@ export interface DailyReward {
}
export interface DailyRewardsPayload {
- date: string;
+ date_utc: string;
feature_enabled: boolean;
rewards: DailyReward[];
claim_totals?: {
@@ -30,9 +30,15 @@ export interface DailyRewardsPayload {
};
}
+export interface ClaimSuccess {
+ category: RewardCategory;
+ coins: number;
+ claimed_at: string;
+}
+
export interface ClaimAllResponse {
status: "ok";
- claimed_categories: RewardCategory[];
+ claimed: ClaimSuccess[];
skipped_categories: { category: RewardCategory; reason: string }[];
}
@@ -44,7 +50,8 @@ interface ClaimOutcome {
interface ClaimAllOutcome {
ok: boolean;
- claimed: RewardCategory[];
+ claimed: ClaimSuccess[];
+ totalClaimedCoins: number;
skipped: { category: RewardCategory; reason: string }[];
error?: string;
}
@@ -92,8 +99,9 @@ export function useDailyRewards() {
const currentTime = isClient ? useNow({ interval: 60_000 }) : ref(new Date());
const needsResetRefresh = computed(() => {
- if (!data.value?.date) return true;
- const dayStart = new Date(`${data.value.date}T00:00:00Z`);
+ const snapshotDay = data.value?.date_utc;
+ if (!snapshotDay) return true;
+ const dayStart = new Date(`${snapshotDay}T00:00:00Z`);
if (Number.isNaN(dayStart.getTime())) return true;
const nextDay = new Date(dayStart);
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
@@ -104,7 +112,9 @@ export function useDailyRewards() {
() => needsResetRefresh.value || rewards.value.some((reward) => reward.status !== "claimed")
);
- const countdownLabel = computed(() => formatResetCountdown(data.value?.date, currentTime.value));
+ const countdownLabel = computed(() =>
+ formatResetCountdown(data.value?.date_utc, currentTime.value)
+ );
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
() => refreshNuxtData(DAILY_REWARDS_KEY),
@@ -210,7 +220,7 @@ export function useDailyRewards() {
const claimAll = async (): Promise => {
if (claimAllBusy.value) {
- return { ok: false, claimed: [], skipped: [], error: "busy" };
+ return { ok: false, claimed: [], totalClaimedCoins: 0, skipped: [], error: "busy" };
}
claimAllBusy.value = true;
@@ -231,17 +241,17 @@ export function useDailyRewards() {
try {
const response = (await POST("/daily-rewards/claim-all")) as ClaimAllResponse;
- if (response?.claimed_categories?.length) {
- const totalCoins = readyRewards
- .filter((reward) => response.claimed_categories.includes(reward.category))
- .reduce((sum, reward) => sum + reward.coins, 0);
+ const claimedCategories = response.claimed.map((item) => item.category);
+ const totalCoins = response.claimed.reduce((sum, item) => sum + item.coins, 0);
+ if (claimedCategories.length) {
resetAnnouncement();
- ariaAnnouncement.value = buildClaimAllAnnouncement(response.claimed_categories, totalCoins);
+ ariaAnnouncement.value = buildClaimAllAnnouncement(claimedCategories, totalCoins);
}
await refreshNuxtData(DAILY_REWARDS_KEY);
return {
ok: true,
- claimed: response?.claimed_categories ?? [],
+ claimed: response.claimed,
+ totalClaimedCoins: totalCoins,
skipped: response?.skipped_categories ?? [],
};
} catch (err: any) {
@@ -251,6 +261,7 @@ export function useDailyRewards() {
return {
ok: false,
claimed: [],
+ totalClaimedCoins: 0,
skipped: [],
error: normalizeError(err),
};
From cb528585199eb9eabf87be91347f964bfe9b6c25 Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:46:54 +0100
Subject: [PATCH 05/10] Fix lab CTA
---
components/dailyRewards/Card.vue | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index 8c7599db..b6d51b71 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -271,7 +271,12 @@ export default defineComponent({
break;
}
case "lab": {
- const challengeId = sample.challenge_id ?? sample.challengeId;
+ const challengeId = pickSampleValue(
+ "challenge_id",
+ "challengeId",
+ "task_id",
+ "taskId"
+ );
const codingChallengeId = pickSampleValue(
"coding_challenge_id",
"codingChallengeId",
@@ -281,13 +286,13 @@ export default defineComponent({
if (challengeId) {
if (codingChallengeId) {
router.push({
- path: `/challenges/QuizCodingChallenge-${challengeId}`,
+ path: `/challenges/QuizCodingChallenge-${String(challengeId)}`,
query: {
codingChallenge: String(codingChallengeId),
},
});
} else {
- router.push(`/challenges/QuizCodingChallenge-${challengeId}`);
+ router.push(`/challenges/QuizCodingChallenge-${String(challengeId)}`);
}
} else {
router.push("/challenges");
From 72b3e19850311b682faea3c1dcc5d38cf30f56fb Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:56:57 +0100
Subject: [PATCH 06/10] Fix lab CTA
---
components/dailyRewards/Card.vue | 34 ++++++++++++++++++++++----------
1 file changed, 24 insertions(+), 10 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index b6d51b71..615e338f 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -271,28 +271,42 @@ export default defineComponent({
break;
}
case "lab": {
- const challengeId = pickSampleValue(
- "challenge_id",
- "challengeId",
- "task_id",
- "taskId"
- );
- const codingChallengeId = pickSampleValue(
+ const rawChallengeId =
+ pickSampleValue("challenge_id", "challengeId", "task_id", "taskId") ??
+ (typeof sample.task === "object" && sample.task !== null
+ ? sample.task.id ??
+ sample.task.challenge_id ??
+ sample.task.challengeId ??
+ sample.task.task_id ??
+ sample.task.taskId
+ : undefined);
+ const challengeId =
+ rawChallengeId !== undefined && rawChallengeId !== null && rawChallengeId !== ""
+ ? String(rawChallengeId)
+ : undefined;
+ const codingChallengeIdValue = pickSampleValue(
"coding_challenge_id",
"codingChallengeId",
"subtask_id",
"subTaskId"
);
+ const codingChallengeId =
+ codingChallengeIdValue !== undefined &&
+ codingChallengeIdValue !== null &&
+ codingChallengeIdValue !== ""
+ ? String(codingChallengeIdValue)
+ : undefined;
+
if (challengeId) {
if (codingChallengeId) {
router.push({
- path: `/challenges/QuizCodingChallenge-${String(challengeId)}`,
+ path: `/challenges/QuizCodingChallenge-${challengeId}`,
query: {
- codingChallenge: String(codingChallengeId),
+ codingChallenge: codingChallengeId,
},
});
} else {
- router.push(`/challenges/QuizCodingChallenge-${String(challengeId)}`);
+ router.push(`/challenges/QuizCodingChallenge-${challengeId}`);
}
} else {
router.push("/challenges");
From 77d1b10f75dd9b569327cd1ad8184f864f601cd2 Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 15:58:34 +0100
Subject: [PATCH 07/10] Lint fixes
---
components/dailyRewards/Card.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index 615e338f..ea4f47ad 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -274,11 +274,11 @@ export default defineComponent({
const rawChallengeId =
pickSampleValue("challenge_id", "challengeId", "task_id", "taskId") ??
(typeof sample.task === "object" && sample.task !== null
- ? sample.task.id ??
+ ? (sample.task.id ??
sample.task.challenge_id ??
sample.task.challengeId ??
sample.task.task_id ??
- sample.task.taskId
+ sample.task.taskId)
: undefined);
const challengeId =
rawChallengeId !== undefined && rawChallengeId !== null && rawChallengeId !== ""
From 978056aa471301969bc553e3850a81488165669f Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 16:17:33 +0100
Subject: [PATCH 08/10] Cope with backend error
---
components/dailyRewards/Card.vue | 57 ++++++++++++++++++++++++++++++--
1 file changed, 54 insertions(+), 3 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index ea4f47ad..a047378d 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -38,7 +38,7 @@
{{ t("DailyRewards.Error") }}
@@ -52,7 +52,7 @@
{{ t("DailyRewards.Disabled") }}
@@ -122,10 +122,57 @@ export default defineComponent({
ariaAnnouncement,
} = useDailyRewards();
+ const isFeatureDisabledError = (err: unknown) => {
+ if (!err || typeof err !== "object") {
+ return false;
+ }
+ const errorLike = err as Record
;
+ const status =
+ errorLike?.response?.status ??
+ errorLike?.statusCode ??
+ errorLike?.status ??
+ errorLike?.response?.statusCode;
+ if (status !== 404) {
+ return false;
+ }
+ const errorCode =
+ errorLike?.response?._data?.error ??
+ errorLike?.response?._data?.code ??
+ errorLike?.data?.error ??
+ errorLike?.data?.code;
+ if (typeof errorCode === "string" && errorCode.toLowerCase().includes("featuredisabled")) {
+ return true;
+ }
+ const detail =
+ errorLike?.response?._data?.detail ??
+ errorLike?.data?.detail ??
+ (typeof errorLike?.message === "string" ? errorLike.message : "");
+ if (typeof detail === "string" && detail.toLowerCase().includes("feature disabled")) {
+ return true;
+ }
+ // Default to treating a 404 response from daily rewards as feature disabled.
+ return true;
+ };
+
const hasReadyRewards = computed(() =>
rewards.value.some((reward) => reward.status === "ready")
);
+ const hasError = computed(() => {
+ const currentError = error.value;
+ if (!currentError) {
+ return false;
+ }
+ return !isFeatureDisabledError(currentError);
+ });
+
+ const featureDisabled = computed(() => {
+ if (isFeatureDisabledError(error.value)) {
+ return true;
+ }
+ return !featureEnabled.value;
+ });
+
const summaryByCategory = computed(() =>
rewards.value.reduce>((acc, reward) => {
acc[reward.category] = reward;
@@ -219,7 +266,9 @@ export default defineComponent({
(pickSampleValue("skill_id", "skillId") ? "skill" : undefined) ??
(pickSampleValue("quiz_id", "quizId") ? "quiz" : undefined);
let solveId =
- pickSampleValue("solve_id", "solveId") ?? pickSampleValue("quiz_id", "quizId");
+ pickSampleValue("solve_id", "solveId") ??
+ pickSampleValue("quiz_id", "quizId") ??
+ pickSampleValue("task_id", "taskId");
if (!solveId) {
if (resolvedQuizzesFrom === "course") {
@@ -332,6 +381,8 @@ export default defineComponent({
claimBusy,
claimAllBusy,
ariaAnnouncement,
+ featureDisabled,
+ hasError,
hasReadyRewards,
handleClaim,
handleClaimAll,
From ec33ee4d98ce073c1f8a18c67fd972fc5f218d0a Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 16:33:12 +0100
Subject: [PATCH 09/10] normalize subtasks detect matchings
---
components/dailyRewards/Card.vue | 62 +++++++++++++++++++++++---------
composables/dailyRewards.ts | 2 +-
2 files changed, 47 insertions(+), 17 deletions(-)
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index a047378d..0557f787 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -260,40 +260,68 @@ export default defineComponent({
break;
}
case "practice": {
+ const normalizeString = (value: unknown) =>
+ typeof value === "string" && value.trim() !== "" ? value.toLowerCase() : undefined;
+ const deriveQuizzesFrom = (value?: string) => {
+ if (!value) return undefined;
+ if (value.includes("course")) return "course";
+ if (value.includes("skill")) return "skill";
+ if (value.includes("quiz")) return "quiz";
+ return undefined;
+ };
+
+ const rawSubtaskType = normalizeString(pickSampleValue("subtask_type", "subtaskType"));
+ const isMatchingSubtask = rawSubtaskType?.includes("matching") ?? false;
+
+ const courseId = pickSampleValue("course_id", "courseId");
+ const skillId = pickSampleValue("skill_id", "skillId");
+ const quizId = pickSampleValue("quiz_id", "quizId");
+ const taskId = pickSampleValue("task_id", "taskId");
+ const rawSolveId = pickSampleValue("solve_id", "solveId");
+
+ const rawQuizzesFrom = normalizeString(pickSampleValue("quizzes_from", "quizzesFrom"));
+ const derivedQuizzesFromFromType = deriveQuizzesFrom(rawSubtaskType);
+
const resolvedQuizzesFrom =
- (pickSampleValue("quizzes_from", "quizzesFrom") as string | undefined) ??
- (pickSampleValue("course_id", "courseId") ? "course" : undefined) ??
- (pickSampleValue("skill_id", "skillId") ? "skill" : undefined) ??
- (pickSampleValue("quiz_id", "quizId") ? "quiz" : undefined);
- let solveId =
- pickSampleValue("solve_id", "solveId") ??
- pickSampleValue("quiz_id", "quizId") ??
- pickSampleValue("task_id", "taskId");
+ rawQuizzesFrom ??
+ (courseId ? "course" : undefined) ??
+ (skillId ? "skill" : undefined) ??
+ (quizId ? "quiz" : undefined) ??
+ derivedQuizzesFromFromType ??
+ "quiz";
+
+ let solveId = rawSolveId ?? quizId ?? taskId;
if (!solveId) {
if (resolvedQuizzesFrom === "course") {
- solveId = pickSampleValue("course_id", "courseId");
+ solveId = courseId;
} else if (resolvedQuizzesFrom === "skill") {
- solveId = pickSampleValue("skill_id", "skillId");
+ solveId = skillId;
} else if (resolvedQuizzesFrom === "quiz") {
- solveId = pickSampleValue("quiz_id", "quizId");
+ solveId = quizId;
}
}
+ const normalizedSolveId =
+ solveId !== undefined && solveId !== null && solveId !== ""
+ ? String(solveId)
+ : undefined;
+
const querySubTaskId = pickSampleValue(
"query_subtask_id",
"querySubTaskId",
+ "matching_id",
+ "matchingId",
"subtask_id",
"subTaskId"
);
- const taskId = pickSampleValue("task_id", "taskId");
const rootSkillId = pickSampleValue("root_skill_id", "rootSkillId", "rootSkillID");
const subSkillId = pickSampleValue("sub_skill_id", "subSkillId", "subSkillID");
- const fallbackSkillId = pickSampleValue("skill_id", "skillId");
+ const fallbackSkillId = skillId;
- if (solveId && resolvedQuizzesFrom) {
+ if (normalizedSolveId) {
const query: Record = {
- quizzesFrom: String(resolvedQuizzesFrom),
+ quizzesFrom: resolvedQuizzesFrom,
};
if (querySubTaskId) {
query.querySubTaskId = String(querySubTaskId);
@@ -308,8 +336,10 @@ export default defineComponent({
query.subSkillID = String(subSkillId);
}
+ const solveBasePath = isMatchingSubtask ? "/matchings" : "/quizzes";
+
router.push({
- path: `/quizzes/solve-${String(solveId)}`,
+ path: `${solveBasePath}/solve-${normalizedSolveId}`,
query,
});
} else if (fallbackSkillId) {
diff --git a/composables/dailyRewards.ts b/composables/dailyRewards.ts
index 2f64cedf..afc45bdd 100644
--- a/composables/dailyRewards.ts
+++ b/composables/dailyRewards.ts
@@ -25,7 +25,7 @@ export interface DailyRewardsPayload {
feature_enabled: boolean;
rewards: DailyReward[];
claim_totals?: {
- available: number;
+ available_coins: number;
claimed_today: number;
};
}
From e350bfbbdf064c2dae709155c1f97b452c18249d Mon Sep 17 00:00:00 2001
From: Morpheus
Date: Sat, 1 Nov 2025 18:49:27 +0100
Subject: [PATCH 10/10] safety updates
---
Agents.md | 2 ++
components/dailyRewards/Card.vue | 10 +++++++++-
2 files changed, 11 insertions(+), 1 deletion(-)
create mode 100644 Agents.md
diff --git a/Agents.md b/Agents.md
new file mode 100644
index 00000000..573d4282
--- /dev/null
+++ b/Agents.md
@@ -0,0 +1,2 @@
+Your instructions are located at ../Agents.md.
+Read the ENTIRE file. They are INCREDIBLY important.
diff --git a/components/dailyRewards/Card.vue b/components/dailyRewards/Card.vue
index 0557f787..6bda2e5a 100644
--- a/components/dailyRewards/Card.vue
+++ b/components/dailyRewards/Card.vue
@@ -274,7 +274,15 @@ export default defineComponent({
const isMatchingSubtask = rawSubtaskType?.includes("matching") ?? false;
const courseId = pickSampleValue("course_id", "courseId");
- const skillId = pickSampleValue("skill_id", "skillId");
+ const rawSkillId = pickSampleValue("skill_id", "skillId");
+ const skillIdsValue = pickSampleValue("skill_ids", "skillIds");
+ const derivedSkillId =
+ Array.isArray(skillIdsValue)
+ ? skillIdsValue.find(
+ (value) => value !== undefined && value !== null && value !== ""
+ )
+ : skillIdsValue;
+ const skillId = rawSkillId ?? derivedSkillId;
const quizId = pickSampleValue("quiz_id", "quizId");
const taskId = pickSampleValue("task_id", "taskId");
const rawSolveId = pickSampleValue("solve_id", "solveId");