diff --git a/app/pages/course/[id].vue b/app/pages/course/[id].vue index 9069ff3..0269fb4 100644 --- a/app/pages/course/[id].vue +++ b/app/pages/course/[id].vue @@ -80,8 +80,16 @@ onMounted(() => { onUnmounted(() => { if (tipTimer) clearInterval(tipTimer); }); +function getTopicProgress(topicId: string): any { + if (!import.meta.client) return null; + try { + const p = localStorage.getItem(`progress:${topicId}`); + return p ? JSON.parse(p) : null; + } catch { return null; } +} + function completedCount(): number { - return course.value?.topics?.filter((t: any) => t.progress?.lessonComplete).length ?? 0; + return course.value?.topics?.filter((t: any) => getTopicProgress(t.id)?.lessonComplete).length ?? 0; } const auditReport = computed(() => { @@ -110,6 +118,55 @@ function scoreColor(score: number): string { const editingTitle = ref(false); const titleDraft = ref(""); +// ── dev dropdown ────────────────────────────────────────────────────────── + +const devOpenId = ref(null); +const devKey = ref(""); +const devPromptTopicId = ref(null); +const devPendingAction = ref<"regenerate-lesson" | "regenerate-audio" | null>(null); +const devLoading = ref(false); +const devMsg = ref<{ text: string; ok: boolean } | null>(null); + +function openDevPrompt(topicId: string, action: "regenerate-lesson" | "regenerate-audio") { + devOpenId.value = null; + devPromptTopicId.value = topicId; + devPendingAction.value = action; + devMsg.value = null; +} + +async function runDevAction() { + if (!devPromptTopicId.value || !devPendingAction.value || devLoading.value) return; + devLoading.value = true; + devMsg.value = null; + + try { + const result = await $fetch<{ status: string }>( + `/api/topics/${devPromptTopicId.value}/dev/${devPendingAction.value}`, + { method: "POST", headers: { "x-license-key": devKey.value } } + ); + + if (result.status === "ready") { + devMsg.value = { text: "Done.", ok: true }; + await refresh(); + setTimeout(() => { + devPromptTopicId.value = null; + devKey.value = ""; + devPendingAction.value = null; + devMsg.value = null; + }, 900); + } else { + devMsg.value = { text: "Returned an error — check server logs.", ok: false }; + } + } catch (err: any) { + devMsg.value = { + text: err?.statusCode === 401 ? "Invalid license key." : "Request failed — check server logs.", + ok: false, + }; + } finally { + devLoading.value = false; + } +} + function startEditTitle() { titleDraft.value = course.value?.title ?? ""; editingTitle.value = true; @@ -332,75 +389,93 @@ async function saveTitle() {
- - +
- - {{ String(idx + 1).padStart(2, "0") }} - + + + {{ String(idx + 1).padStart(2, "0") }} + -
-
- {{ topic.title }} - done - - Not yet generated - - Generating... - - Error +
+
+ {{ topic.title }} + done + + Not yet generated + + Generating... + + Error +
+ +
+
+ +
+ + {{ ['', 'intro', 'basic', 'intermediate', 'advanced', 'expert'][topic.difficulty] }} + + + · quiz {{ getTopicProgress(topic.id).quizScore }}/4 + + + ~${{ topic.lessonCost.toFixed(2) }} + +
-
-
- -
- - {{ ['', 'intro', 'basic', 'intermediate', 'advanced', 'expert'][topic.difficulty] }} - - - · quiz {{ topic.progress.quizScore }}/4 - - - ~${{ topic.lessonCost.toFixed(2) }} - + + + + + + + +
+ +
+ +
- - - - - - - +
+
@@ -408,6 +483,32 @@ async function saveTitle() {
+ + + +
+
+

{{ devPendingAction === 'regenerate-lesson' ? 'Regenerate Lesson' : 'Regenerate Audio' }}

+

Enter your license key to continue.

+ +

{{ devMsg.text }}

+
+ + +
+
+
+
+ @@ -640,6 +741,10 @@ async function saveTitle() { /* topic list */ .topic-list {} +.topic-item { + position: relative; +} + .topic-row { display: flex; align-items: center; @@ -869,6 +974,122 @@ async function saveTitle() { font-size: 10px; color: var(--text-3); letter-spacing: 0.04em; } +/* dev dropdown */ +.dev-wrap { + position: absolute; + top: 50%; + right: 2.5rem; + transform: translateY(-50%); + z-index: 10; +} + +.dev-cog { + background: none; + border: 1px solid var(--border); + border-radius: 5px; + width: 26px; height: 26px; + display: flex; align-items: center; justify-content: center; + font-size: 13px; + cursor: pointer; + color: var(--text-3); + transition: border-color 0.15s, color 0.15s; + opacity: 0; +} +.topic-item:hover .dev-cog, +.dev-cog--open { opacity: 1; } +.dev-cog:hover, +.dev-cog--open { border-color: var(--border-2); color: var(--text); } + +.dev-drop { + position: absolute; + top: calc(100% + 5px); + right: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 16px oklch(0% 0 0 / 0.1); + min-width: 170px; + z-index: 50; + overflow: hidden; +} + +.dev-item { + display: block; width: 100%; text-align: left; + background: none; border: none; + padding: 9px 13px; + font-size: 12px; color: var(--text-2); + cursor: pointer; + transition: background 0.1s; +} +.dev-item:hover { background: var(--surface-2); } +.dev-item + .dev-item { border-top: 1px solid var(--border); } + +/* dev modal */ +.dev-overlay { + position: fixed; inset: 0; z-index: 200; + background: rgba(0,0,0,0.35); + display: flex; align-items: center; justify-content: center; + padding: 2rem; +} + +.dev-modal { + background: var(--surface); + border-radius: 12px; + padding: 1.5rem; + width: 100%; max-width: 340px; + display: flex; flex-direction: column; gap: 0.875rem; + box-shadow: 0 8px 32px oklch(0% 0 0 / 0.18); +} + +.dev-modal-title { + font-size: 14px; font-weight: 600; color: var(--text); margin: 0; +} + +.dev-modal-sub { + font-size: 13px; color: var(--text-3); margin: 0; +} + +.dev-modal-input { + width: 100%; padding: 9px 11px; + border: 1.5px solid var(--border); + border-radius: 7px; + font-size: 13px; color: var(--text); + background: var(--bg); + outline: none; + transition: border-color 0.15s; + box-sizing: border-box; +} +.dev-modal-input:focus { border-color: var(--accent); } + +.dev-modal-msg { font-size: 12px; margin: 0; } +.dev-modal-msg--ok { color: var(--green); } +.dev-modal-msg--err { color: oklch(50% 0.18 25); } + +.dev-modal-actions { display: flex; gap: 0.625rem; justify-content: flex-end; } + +.dev-modal-cancel { + background: none; border: 1px solid var(--border); + border-radius: 7px; padding: 7px 14px; + font-size: 12px; color: var(--text-3); cursor: pointer; + transition: border-color 0.15s; +} +.dev-modal-cancel:hover:not(:disabled) { border-color: var(--border-2); } +.dev-modal-cancel:disabled { opacity: 0.5; cursor: not-allowed; } + +.dev-modal-run { + background: var(--accent); color: #fff; + border: none; border-radius: 7px; + padding: 7px 18px; + font-size: 12px; font-weight: 500; + cursor: pointer; transition: opacity 0.15s; +} +.dev-modal-run:hover:not(:disabled) { opacity: 0.85; } +.dev-modal-run:disabled { opacity: 0.45; cursor: not-allowed; } + +.dev-fade-enter-active { transition: opacity 0.15s ease; } +.dev-fade-leave-active { transition: opacity 0.1s ease; } +.dev-fade-enter-from, .dev-fade-leave-to { opacity: 0; } + /* retry button on error topics */ .topic-retry-btn { font-size: 11px; letter-spacing: 0.06em; diff --git a/app/pages/index.vue b/app/pages/index.vue index 15987ad..1d32f7d 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -9,7 +9,7 @@ interface CourseSummary { stage: string | null createdAt: string topicCount: number - completedCount: number + topicIds: string[] costAI: number | null costAudio: number | null organisation: string | null @@ -111,9 +111,19 @@ async function confirmDelete() { } } +function completedCount(course: CourseSummary): number { + if (!import.meta.client) return 0; + return course.topicIds.filter(id => { + try { + const p = localStorage.getItem(`progress:${id}`); + return p ? JSON.parse(p).lessonComplete === true : false; + } catch { return false; } + }).length; +} + function progressPct(course: CourseSummary): number { if (!course.topicCount) return 0; - return Math.round((course.completedCount / course.topicCount) * 100); + return Math.round((completedCount(course) / course.topicCount) * 100); } // deterministic warm color from subject string @@ -205,7 +215,7 @@ function subjectColor(subject: string): string {
- {{ course.completedCount }} / {{ course.topicCount }} + {{ completedCount(course) }} / {{ course.topicCount }}