Video 3
The AI App for Your Class Doesn’t Know You. Build Your Own (Using Claude Cowork)
Build a custom AI dashboard for your classes on Claude: it reads your syllabus and your messy lecture notes, tracks what you actually understand topic by topic, flags the gaps before the exam does, and tells you exactly what to study next.
# Course Dashboard — paste this into Claude Cowork
This builds the course dashboard from the video: **one table per course that becomes the single source of truth, rendered as a dashboard you talk to.** It has three parts — a **Weekly Debrief** that looks back and learns how you're doing, an **Upcoming Week** view that gets you ready to participate in your next two classes, and a **Long-Term Success** view that keeps you on track for exams and papers.
**How it stays flexible:** both the *table* and the *dashboard itself* are shaped to your course, not poured into a fixed mold. Claude infers the table's schema from your syllabus, then adapts the dashboard's sections to match — a seminar, a problem-set course, and a lecture course each come out differently. Every "smart" line (the reads, the reflection questions, the pre-class drafts, the study actions) is **written by Claude with judgment and cached in the table**; the browser only looks it up and never guesses. Inputs are usually partial, so it builds from what's there, marks the rest "to be announced," and fills in as you add materials. You update it by talking, never by hand.
**Before you paste this, you need one thing:**
1. A **folder for a single course**, connected to this Cowork project. That's it — it can be empty. When you paste this prompt, Claude sets up the folders (**Syllabus & Info, Lecture Slides, Readings, My Notes, Assignments, Exams & Study**) and builds an empty dashboard, then asks you to drop your **syllabus** into *Syllabus & Info* to fill it in. Add slides, readings, and notes over the term.
Connecting **Gmail** helps too — it catches deadline changes a syllabus never knows about (a moved exam, a new reading) — but the syllabus is the one thing that matters to start.
**How to use:** copy this entire file, paste it into a Claude Cowork chat that's connected to your course folder, and send it. Claude does the rest. After it's built, you don't edit anything by hand — you just talk to it.
---
You are building my dashboard for **one university course**, inside the folder you have access to. You are my private companion for this one course, optimized entirely around helping *me* learn it.
The guiding principle is simple: **`course-model.json` is the single source of truth.** It holds the course as structured data. Every view is just a drawing of that table — and every "smart" part (each Coach read, the debrief questions, the "what I'm noticing" notes, each lecture's draft discussion post and class questions, the long-term actions) is a **cached entry you write from the table with judgment**, so the dashboard gives real feedback, renders instantly, and updates the moment I tell you something changed. Do the steps below, and never let the browser do the thinking.
## Step 0 — Scaffold the folders and build the empty shell
Confirm you can see and read my connected course folder, then do these in order.
**1. Create the standard folders** if they're not already there (empty is fine — no readme), so there's a clear home for each kind of file:
~~~
<my course folder>/
Syllabus & Info/ — syllabus + any schedule / Canvas export (start here)
Lecture Slides/ — slide decks
Readings/ — assigned + optional readings
My Notes/ — my notes, whiteboard photos, screenshots, reflections, graded feedback
Assignments/ — problem sets, paper/project prompts + my drafts
Exams & Study/ — past/sample exams, study guides, rubrics, index cards
dashboard/ — you generate this; I don't touch it
~~~
Treat these folders as **strong hints about what each file is**, but don't depend on them — if I've misfiled something, read it anyway and infer from its content. Deadline changes come from my connected Gmail, so there's no email folder.
**2. Build the empty shell right away — before asking me for anything.** Write `dashboard/course-model.json` with just a skeleton: a best-guess `course.title` from the folder name, `today` set to today's date, an empty `schedule`, an empty `learningModel`, and empty `views`. Render the dashboard artifact from the reference build (Step 4). With no schedule yet it shows a friendly onboarding shell — a "0% built" coverage bar and an "add your syllabus" message — so I can see my dashboard already exists.
**3. Then ask me to populate the folders, starting with the syllabus.** Say something like:
> Your dashboard shell is ready. Add your **syllabus** to the **Syllabus & Info** folder (PDF, Word, a link, even a screenshot) and tell me — that's all I need to build out your schedule, deadlines, and coaching. Then drop slides, readings, and notes into the other folders whenever you have them, and it fills in.
Wait for the syllabus. Don't manufacture a long questionnaire and don't invent course content. Once the syllabus is there, continue to Step 1.
## Step 1 — Read everything and audit the course
Read the **syllabus first** (in *Syllabus & Info*) — it defines the structure: the schedule, learning objectives, assignments, exams, grading, recurring requirements, and expectations. Then read everything else, using the folders as a guide to what each file is: *Lecture Slides* → slides, *Readings* → readings, *My Notes* → my notes, whiteboard photos, and feedback, *Assignments* → coursework, *Exams & Study* → exam prep. If a file is misfiled, read it anyway and infer from its content. If Gmail is connected, scan it for anything that moves a date (a professor email, a Canvas announcement).
As you read, **audit what makes this course *this* course.** You are about to design a table, and its shape must fit the course. Ask:
- How does it meet, and how is each session framed (a driving question? a topic? a case)?
- How is it graded, and what are the **recurring requirements**? A discussion post? A weekly response paper? Problem sets? Lab reports? Reading responses? Each course is different — capture the real one.
- What are the **major milestones** — midterms, a final, papers, a project, a presentation — and what format are they (multiple choice, essays, a research paper, a studio critique)?
- Are there **distinctive study artifacts or rules** — an allowed index card, an open-book policy, a rubric, sample exams, a required question format?
## Step 2 — Build the table (`course-model.json`), with an INFERRED schema
Save a structured representation at `dashboard/course-model.json`. This is the source of truth.
**Infer the schema from the audit — do not impose a fixed one.** A lecture course with discussion posts and two midterms should not have the same shape as a seminar with weekly response papers or a methods course with problem sets. Include an `_schema_note` string explaining the shape you chose and why. Capture whatever this course actually has — for most courses that includes some of:
- `course` — title, code, term, instructor, meeting days, and the **grading breakdown** (with sub-components, e.g. participation = posts + attendance).
- `today` (ISO), `termStart`, `termEnd`.
- `requirements` — the **recurring** obligations, each with its cadence and exact rule (e.g. "submit one question about the reading by 5pm the day before lecture"). This is course-specific — get it right.
- `assessments` — the milestones (exams, papers, project, presentation), each with date, weight, what it covers, and format (and any rule like "one 3×5 index card").
- `schedule` — one row per class meeting: `n`, `date`, `type` ("lecture" | "exam" | "noclass"), `title`, the driving question `q`, and `reading` ({ `cite`, optional list, a 1–2 sentence `gist`, and the `theme` — how it connects to the course's big questions). Record no-class dates as `type: "noclass"`. **Handle missing detail honestly:** leave `title`/`reading` null when the syllabus doesn't give them yet. If a session is *expected* to have a reading that simply hasn't been posted, set `readingTBD: true` (distinct from leaving `reading` null for a session with genuinely none). These flags drive the coverage meter and the "to be posted" placeholders instead of you inventing content.
- `learningModel` — an evolving, **probabilistic** read of me: `concepts` (each with `lec`, a `since` date it became knowable, a `conf` 0–1, and a `note`) and `misconceptions` (each with `lec`, `since`, an optional `resolved` date, `prio`, a `detail`, and `src` provenance tags). It starts small and grows from my notes and our chats.
Rules: tag **provenance** (syllabus / note / you / lecture) so I can trust each claim. Give every concept/misconception a **`since`** date so nothing shows before it was knowable, and a **`resolved`** date instead of deleting it, so history stays honest. Where the syllabus is vague (an "end of finals week" deadline), make your best call and flag it. Never invent a date silently.
## Step 3 — Write the cached "smart view" tables (the important part)
This is the upgrade. The coaching is **not** computed by the browser — a hardcoded rule writes generic, sometimes-wrong advice. Instead, **you (the LLM) write these from the table, using judgment, and cache them under `views`** so the dashboard renders them instantly by lookup. Respect **temporal integrity**: write each week's entries using only what's known by that week (never leak a later lecture's insight into an earlier week). Store:
- **`views.coach.upcoming`** — `{ "<weekMonday>": "<1–3 sentence read of the coming week: what to read, what's due, the through-line>" }`.
- **`views.coach.debrief`** — `{ "<weekMonday>": "<1–2 sentence read of the week just finished>" }`.
- **`views.coach.longterm`** — `"<a single as-of-now read of where I stand for the next big milestone and the highest-leverage move>"`.
- **`views.debrief`** — `{ "<weekMonday>": { "questions": ["2–3 focused reflective questions about what was clear/confusing this week"], "noticing": [ {"type":"good"|"flag", "text":"a tentative, evidence-based observation about my understanding"} ] } }`. Write the noticing from the `learningModel`, phrased tentatively ("I suspect…", "looks solid").
- **`views.upcoming`** — `{ "<lectureN>": { "deliverable": {"label":"…","draft":"…","due":"…"}, "questions": ["2–3 things I could raise in class"], "prep": "<one line on what to know for any attendance/participation check>", "diagram": "<optional inline SVG>" } }`. **Every field is optional — include only what the course actually has.**
- `deliverable` is the required pre-class work, and its shape is driven by the syllabus's recurring requirement. **If the course has no pre-class submission, omit `deliverable` entirely** — do not invent a discussion post. Set `label` to what it actually is and `draft` to a real draft of it: e.g. a discussion course → `{"label":"Draft your discussion post","draft":"<a sharp question about the reading>","due":"5pm the day before"}`; a seminar → `{"label":"Draft your response paper","draft":"<a thesis + first move>","due":"Sunday 11:59pm"}`; a problem-set course → `{"label":"Problem set 5","draft":"<which problems, what to start with>","due":"Friday 5pm"}`. A lecture course with only attendance and exams gets **no** `deliverable`.
- If I've added a slide or a photo of a whiteboard/board diagram for a lecture, you may **redraw it cleanly as an inline SVG** in `diagram` — use the palette (cream/clay/ink), no external fonts — and the card renders it under "The board, cleaned up." Omit when there's nothing to draw.
- **`views.longterm`** — `{ "actions": [ {"title":"…","body":"…","src":["syllabus"|"you"|…]} ], "examStrategy": "<how to prepare for the milestone's format>" }`. Pull the actions from real gaps in the `learningModel` and the milestone's requirements.
- **`views.notes`** — `{ "<lectureN>": ["<cleaned-up bullet>", "…"] }`. These are **my actual notes, cleaned up — not a short summary.** Read the note I added and rewrite it into a faithful, detailed set of bullets (usually 6–10) that preserves the substance — the frameworks, cases, names, dates, and examples I wrote down — improving clarity and fixing typos, but staying close to what I actually captured. Put my own confusions/open questions in their own bullets and **start them with "Flagged —" or "Open question —"** (the renderer highlights those in red). Write these only for lectures where I've added notes (record the file under that lecture's `materials`). The Weekly Debrief shows them as a bulleted "YOUR NOTES" block under the session. Also let the notes update the `learningModel` (a note revealing a mix-up becomes a flagged misconception; a confident, correct note raises confidence).
Write entries for the current week and the near horizon (through the end of term if you can). Any week you don't write shows a graceful empty state — that's fine, but cover at least the current and next couple of weeks richly.
## Step 4 — Render the dashboard (adapt the reference build to the course)
Create a Cowork artifact whose id and name include **this specific course**, never a generic label — e.g. id `qm2-dashboard` and name “QM II — Course Dashboard,” or id `gpps463-dashboard` and name “Politics of SE Asia — Course Dashboard.” Set the name by replacing `COURSE NAME` in the `cowork-artifact-meta` block at the top of the HTML (and the `<title>`). The HTML at the bottom of this file is a **reference build**, not a fixed template. The fastest path is: start from it, drop in the model, and **reshape it to fit this course** — because a lecture course, a seminar, and a lab should not look identical. The design system and the guardrails below are fixed; the *sections* are yours to arrange.
- **Drop in the model.** Replace `/*MODEL*/` with the entire `course-model.json` object.
- **Keep the fixed parts:** the three tabs (**Weekly Debrief**, **Upcoming Week**, **Long-Term Success**) as the default intents; the week navigator on the first two; the cream/clay design system (tokens, type, spacing); and the principle that all narrative is a pure lookup into the cached `views`.
- **Adapt the variable parts to the course.** Relabel and add/remove sections so they match what the syllabus actually has. The reference's Upcoming Week already renders each block only when its data exists (reading, `deliverable`, prep, questions, diagram) — so if you built the table honestly, unused blocks simply don't appear. Go further when a course needs it: a problem-set course might want a "this week's problem set" section; a paper-driven seminar a "thesis workshop" block; a studio a "crit prep" block. Invent sections when the course calls for them, and cut ones that don't apply.
**Hard guardrails (never break these):**
- **CSP-safe:** no inline `onclick`/`onkeydown`/`oninput` — the artifact sandbox blocks them. Bind everything with `addEventListener` and event delegation (as the reference does).
- **Light mode**, self-contained (inline CSS/JS, no external fonts or scripts).
- **The browser never guesses.** It does only mechanical work (which meetings are next, dates, bars); all judgment/narrative is looked up from `views`.
- Keep **provenance**, **temporal integrity** (nothing shows before its `since`), and **probabilistic language**.
**Before you finish, verify it.** Open/inspect the rendered artifact: confirm all three tabs render, the week nav works, the current week shows the right meetings, and — importantly — that no section appears that this course doesn't have (e.g. no "discussion post" block for a course without one). Fix anything off before telling me it's ready.
## Handling incomplete or evolving inputs
Most syllabi are partial, and that's normal — a course may list readings but no per-day topics, or topics but no readings, or just a schedule of dates. Detail arrives week by week as I drop materials into the folder. Build for that:
- **Never fabricate.** If a lecture's topic or reading isn't in my materials yet, leave it blank in the table (`title`/`reading` null). The dashboard shows a plain "topic TBA / add materials when released" placeholder rather than an invented one.
- **Fill in as it arrives.** When I add a slide deck, a reading, or a Canvas update, fold it into the table and regenerate the affected `views` — the placeholders turn into real content.
- **Reshape when new material changes what's possible.** If a course turns out to have a requirement you didn't see at first (a response paper, a quiz, a project), add that section to the dashboard then — don't force everything into the first shape you guessed. The view is allowed to grow with the course.
- **Say what's missing.** If the syllabus is too thin to build much, build what you can and tell me exactly what to add next (usually: per-lecture topics, or the readings) so the dashboard gets more useful.
- **Show the progress.** The dashboard's coverage meter reflects how filled-in the course is (a "% built" bar) and names what's still missing ("still waiting on 4 lecture topics and 9 readings"). This makes a thin course read as *in progress*, not broken — and it climbs visibly as I add materials, which is the point.
## Step 5 — Keep it live (talk to it; never hand-edit)
From here the dashboard is a snapshot I refresh by talking to you.
- When I tell you something about my understanding — "I finally get TFP," "Lecture 8 lost me," "the RD design still confuses me" — update the `learningModel` (raise/lower `conf`, mark a misconception `resolved` with today's date, set provenance to "you"), **rewrite the affected cached views** (the debrief noticing, the long-term actions), and re-render.
- When something about the course changes — "the midterm moved to the 9th," "there's a new reading" — update the `schedule`/`assessments` row, rewrite affected views, re-render.
- The **Weekly Debrief's questions are answered in chat.** When I reply, fold my answers into the learning model and update the dashboard.
- **When I add my notes** (a text file, a photo of the board, a screenshot) for a session, attach the file under that lecture's `materials`, write a short recap into `views.notes`, and update the `learningModel` from what the notes reveal — then re-render. My notes then show up as a "YOUR NOTES" recap in that week's Debrief. (If I hand you a whiteboard/slide diagram, you may also redraw it cleanly into the lecture's `diagram`.)
- When I ask a question — "what's due this week?", "am I ready for the midterm?" — answer in plain words straight from the table. Read it; don't guess.
Always treat `course-model.json` as the one source of truth, and never let the dashboard or any cached view drift from it.
## Step 6 — Put it on autopilot (a weekly briefing)
Set up a scheduled task so the dashboard keeps itself current and checks in with me.
First ask whether I want it weekly or daily and at what time — **default to Sunday evening**, since that's when I plan my week. Then create a scheduled task that, on that cadence:
1. Re-reads my course folder (and Gmail/Canvas if connected) and updates `course-model.json` — new materials folded in, deadlines reconciled (newer source wins), ambiguities flagged, my "I finished/understood X" updates applied.
2. **Regenerates the cached `views`** with fresh judgment (the same pass as Step 3), respecting temporal integrity.
3. Re-renders the dashboard artifact (the one replacement from Step 4).
4. Sends me a short, skimmable briefing: my next two class meetings and what to prep, upcoming deadlines, the single highest-leverage thing to do now for my next big milestone, and anything that changed since last time.
One thing to tell me: a scheduled task only runs while my desktop app is open and awake, so it fires the next time both are true.
## Each new week / new term
- **Each week:** advance `today`, sweep the folder for anything I've added, and regenerate the cached views for the new current week and horizon.
- **New term:** when I drop new syllabi/materials and say "rebuild," re-read from scratch, rebuild `course-model.json` with the new term's structure (re-inferring the schema — it may differ), rebuild the views, and re-render. Keep old items as history if I want it, or clear them — ask if it's unclear.
I never edit the table or the template by hand. I can start on any day, in any term; the dashboard centers itself on today automatically.
## The reference build
This is a complete, working dashboard to **start from and adapt** (Step 4), not a template to paste verbatim. At minimum, replace `/*MODEL*/` with your `course-model.json`. Then reshape the sections to fit the course, keeping the design system and the guardrails intact. Everything here renders each block only when its data exists, so an honest table already hides what doesn't apply.
```html
<!doctype html>
<script type="application/json" id="cowork-artifact-meta">
{ "name": "COURSE NAME — Course Dashboard", "schemaVersion": 1, "description": "Personal course dashboard, rendered from course-model.json." }
</script>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Course Dashboard</title>
<style>
:root{ color-scheme: light;
--cream:#F6F4EE; --card:#FFFFFF; --card2:#FBF9F3; --line:#E8E3D8; --line2:#F0ECE2;
--ink:#211F1B; --ink2:#524E46; --mut:#8A847A;
--clay:#D97757; --clay-deep:#BE5C3D; --clay-soft:#F3E2D8;
--school:#3F6A86; --life:#5E7A57; --hi:#BE4A30; --med:#C2873B; --lo:#B5AE9F;
--hibg:#F8EAE2; --medbg:#F8F0DF;
--serif:"Tiempos Text",Georgia,"Times New Roman",serif;
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--shadow:0 1px 2px rgba(33,31,27,.04), 0 10px 26px -16px rgba(33,31,27,.18);
}
*{box-sizing:border-box;}
body{margin:0;background:var(--cream);color:var(--ink);font:14px/1.55 var(--sans);-webkit-font-smoothing:antialiased;}
.wrap{max-width:1040px;margin:0 auto;padding:22px 20px 44px;}
.topbar{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap;}
.brand{display:flex;gap:11px;align-items:center;}
.mark{display:flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:11px;background:var(--clay-soft);flex:none;}
h1{font-family:var(--serif);font-size:24px;font-weight:600;letter-spacing:-.01em;margin:0;line-height:1;}
.sub{color:var(--mut);font-size:12px;margin-top:5px;}
.tabs{display:flex;gap:3px;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:4px;box-shadow:var(--shadow);}
.tab{border:0;background:transparent;color:var(--ink2);padding:7px 13px;border-radius:9px;cursor:pointer;font-size:12.5px;font-weight:600;font-family:var(--sans);}
.tab:hover{background:var(--card2);}
.tab.active{background:var(--clay);color:#fff;}
.weeknav{display:flex;align-items:center;gap:10px;background:var(--card);border:1px solid var(--line);border-radius:11px;padding:7px 11px;margin-bottom:14px;box-shadow:var(--shadow);}
.navbtn{border:1px solid var(--line);background:var(--card2);border-radius:8px;width:30px;height:28px;cursor:pointer;font-weight:700;color:var(--ink2);font-size:15px;line-height:1;}
.navbtn:hover:not(:disabled){border-color:var(--clay);color:var(--clay);}
.navbtn:disabled{opacity:.35;cursor:default;}
.wklabel{font-family:var(--serif);font-size:14px;font-weight:600;}
.pastflag,.futflag{font-size:9.5px;font-weight:800;letter-spacing:.04em;border-radius:6px;padding:3px 8px;text-transform:uppercase;}
.pastflag{color:var(--school);background:#E3EBF1;}
.futflag{color:var(--clay-deep);background:var(--clay-soft);}
.backnow{margin-left:auto;border:0;background:transparent;color:var(--clay-deep);font-size:12px;font-weight:700;cursor:pointer;font-family:var(--sans);}
.backnow:hover{text-decoration:underline;}
.callout{background:var(--clay-soft);border:1px solid #EBCBB9;border-radius:13px;padding:14px 16px;margin-bottom:16px;box-shadow:var(--shadow);}
.callout.plain{background:var(--card2);border-color:var(--line);}
.ctag{font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:800;color:var(--clay-deep);margin-bottom:7px;}
.callout.plain .ctag{color:var(--mut);}
.callout p{margin:0;color:#5c3826;font-size:13.5px;line-height:1.55;}
.callout.plain p{color:var(--ink2);}
.callout b{color:var(--clay-deep);}
.callout.plain b{color:var(--ink);}
.view{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:16px 18px;box-shadow:var(--shadow);margin-bottom:15px;}
.sectlbl{font-size:10.5px;text-transform:uppercase;letter-spacing:.07em;color:var(--mut);font-weight:800;margin:0 0 12px;}
.sectlbl .soft{font-weight:600;text-transform:none;letter-spacing:0;color:var(--mut);}
.datepill{font-size:10.5px;font-weight:800;letter-spacing:.05em;color:var(--clay-deep);background:var(--clay-soft);border-radius:7px;padding:5px 9px;flex:none;}
.datepill.exam{color:var(--hi);background:var(--hibg);}
.lhead{display:flex;align-items:center;gap:11px;margin-bottom:4px;flex-wrap:wrap;}
.ltitle{font-family:var(--serif);font-size:17px;font-weight:600;letter-spacing:-.01em;}
.readrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:9px 0 6px;font-size:13px;color:var(--ink2);}
.badge{font-size:9.5px;font-weight:800;border-radius:5px;padding:2px 6px;letter-spacing:.04em;}
.b-read{background:#E3EBF1;color:#355A73;} .b-exam{background:var(--hibg);color:var(--hi);}
.b-you{background:var(--clay-soft);color:var(--clay-deep);} .b-note{background:#E6EEDF;color:#4B6340;} .b-slide{background:#E3EBF1;color:#355A73;} .b-syllabus{background:#F1E4EE;color:#7A4A6B;}
.due{font-size:11px;font-weight:700;color:var(--hi);background:var(--hibg);border-radius:6px;padding:3px 8px;}
.msub{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:var(--mut);font-weight:800;margin:13px 0 5px;}
.prose{font-size:13px;color:var(--ink2);line-height:1.55;}
.prov{font-size:10.5px;color:var(--mut);margin-top:4px;}
.split{display:grid;grid-template-columns:1fr 1fr;gap:18px;}
@media(max-width:640px){.split{grid-template-columns:1fr;}}
ul.q{list-style:none;margin:0;padding:0;}
ul.q li{position:relative;padding-left:16px;margin:6px 0;font-size:12.5px;color:var(--ink2);line-height:1.5;}
ul.q li:before{content:"";position:absolute;left:0;top:8px;width:6px;height:6px;border-radius:50%;background:var(--clay);}
.draft{background:var(--card2);border:1px solid var(--line2);border-radius:11px;padding:11px 13px;}
.draft .ex{font-family:var(--serif);font-size:13px;color:var(--ink2);line-height:1.5;font-style:italic;}
.diagram{margin-top:4px;border:1px solid var(--line2);border-radius:12px;background:#fff;padding:10px;}
.diagram svg{display:block;width:100%;height:auto;}
.cov{display:flex;gap:11px;align-items:flex-start;padding:9px 0;border-bottom:1px solid var(--line2);}
.cov:last-child{border-bottom:0;}
.cov .dp{min-width:78px;text-align:center;}
.covttl{font-family:var(--serif);font-size:14px;font-weight:600;color:var(--ink);}
.covmeta{font-size:12px;color:var(--ink2);margin-top:2px;line-height:1.5;}
.yournotes{margin-top:8px;background:var(--clay-soft);border:1px solid #EBCBB9;border-radius:10px;padding:9px 12px;}
.ynhead{font-size:10px;text-transform:uppercase;letter-spacing:.05em;font-weight:800;color:var(--clay-deep);margin-bottom:7px;}
.ynhead .badge{margin-right:5px;}
.ynlist{margin:0;padding-left:16px;}
.ynlist li{font-size:12px;color:#5c3826;line-height:1.55;margin:5px 0;}
.ynlist li.flag{color:var(--hi);}
.ynp{font-size:12px;color:#5c3826;line-height:1.55;}
.qq{font-family:var(--serif);font-size:14px;color:var(--ink);margin:0;padding:9px 0;border-bottom:1px solid var(--line2);line-height:1.5;}
.qq:last-child{border-bottom:0;}
.notice{border:1px solid var(--line2);background:var(--card2);border-left:3px solid var(--med);border-radius:0 11px 11px 0;padding:11px 13px;margin:10px 0;font-size:12.5px;line-height:1.5;}
.notice.good{border-left-color:var(--life);}
.notice b{color:var(--ink);}
.mgrid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:15px;}
@media(max-width:600px){.mgrid{grid-template-columns:1fr;}}
.mile{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 15px;box-shadow:var(--shadow);}
.mtop{display:flex;justify-content:space-between;align-items:baseline;gap:8px;}
.mname{font-family:var(--serif);font-size:15px;font-weight:600;}
.mcount{font-size:10.5px;font-weight:800;color:var(--hi);background:var(--hibg);border-radius:6px;padding:3px 7px;white-space:nowrap;}
.mcount.soft{color:var(--ink2);background:var(--line2);}
.mmeta{font-size:11.5px;color:var(--mut);margin-top:4px;line-height:1.4;}
.crow{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid var(--line2);}
.crow:last-child{border-bottom:0;}
.crow.up{opacity:.6;}
.cinfo{flex:1;}
.cname{font-size:13px;font-weight:600;color:var(--ink);}
.cname .lx{font-family:var(--serif);color:var(--mut);font-weight:600;font-size:11.5px;margin-right:5px;}
.bar{height:7px;border-radius:7px;background:var(--line2);overflow:hidden;margin-top:5px;width:100%;}
.bar>span{display:block;height:100%;border-radius:7px;}
.cnote{font-size:11px;color:var(--mut);margin-top:4px;}
.status{font-size:11px;font-weight:700;white-space:nowrap;flex:none;}
.st-work{color:var(--med);} .st-ok{color:var(--life);} .st-soon{color:var(--mut);}
.action{background:var(--card2);border:1px solid var(--line2);border-left:3px solid var(--clay);border-radius:0 11px 11px 0;padding:11px 13px;margin:9px 0;}
.action .at{font-size:13px;font-weight:600;color:var(--ink);}
.action .am{font-size:12px;color:var(--ink2);margin-top:3px;line-height:1.5;}
.hint{font-size:12px;color:var(--ink2);line-height:1.55;background:var(--card2);border:1px solid var(--line2);border-radius:12px;padding:11px 14px;}
.hint .ct{font-size:10px;text-transform:uppercase;letter-spacing:.07em;font-weight:800;color:var(--mut);margin-bottom:5px;}
.hint em{color:var(--clay-deep);font-style:normal;font-weight:600;}
.emptyv{color:var(--mut);font-size:12.5px;padding:6px 0;}
.setup{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:10px 13px;margin-bottom:14px;box-shadow:var(--shadow);}
.setrow{display:flex;align-items:center;gap:10px;}
.setlbl{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;font-weight:800;color:var(--mut);white-space:nowrap;}
.setbar{flex:1;height:7px;background:var(--line2);border-radius:7px;overflow:hidden;}
.setbar>span{display:block;height:100%;background:var(--clay);border-radius:7px;}
.setpct{font-size:11px;font-weight:700;color:var(--clay-deep);white-space:nowrap;}
.setnote{font-size:11.5px;color:var(--ink2);margin-top:6px;line-height:1.5;}
.setnote em{color:var(--clay-deep);font-style:normal;font-weight:600;}
.foot{text-align:center;margin-top:12px;font-size:11px;color:var(--mut);}
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<div class="brand">
<span class="mark"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M12 1.6c.7 6 4.8 10.1 10.4 10.4-5.6.3-9.7 4.4-10.4 10.4-.7-6-4.8-10.1-10.4-10.4C7.2 11.7 11.3 7.6 12 1.6Z" fill="#D97757"/></svg></span>
<div>
<h1 id="ctitle">Course dashboard</h1>
<div class="sub" id="sub"></div>
</div>
</div>
<div class="tabs" id="tabs">
<button class="tab active" data-tab="debrief">Weekly Debrief</button>
<button class="tab" data-tab="upcoming">Upcoming Week</button>
<button class="tab" data-tab="longterm">Long-Term Success</button>
</div>
</div>
<div id="setup"></div>
<div class="weeknav" id="weeknav"></div>
<div id="view"></div>
<div class="foot">A snapshot rendered from your course table. All narrative is written by your coach and cached — update it by talking to me in the chat.</div>
</div>
<script>
const M = /*MODEL*/;
const TODAY=M.today, COURSE=M.course, SCHED=M.schedule, LM=M.learningModel, V=M.views,
ASSESS=M.assessments||[], REQ=M.requirements||[], TERM_START=M.termStart, TERM_END=M.termEnd;
const CONCEPTS=(LM&&LM.concepts)||[], MISC=(LM&&LM.misconceptions)||[];
const DOW=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
const MON=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
function parseD(s){const[a,b,c]=s.split("-").map(Number);return new Date(a,b-1,c);}
function iso(d){return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0");}
function addDays(d,n){const x=new Date(d);x.setDate(x.getDate()+n);return x;}
function fmt(d){return DOW[d.getDay()]+" "+MON[d.getMonth()]+" "+d.getDate();}
function fmtShort(d){return DOW[d.getDay()].toUpperCase()+" · "+MON[d.getMonth()]+" "+d.getDate();}
function esc(s){return (s==null?"":String(s)).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
function daysBetween(a,b){return Math.round((parseD(b)-parseD(a))/864e5);}
function meetingsBetween(a,b){return SCHED.filter(m=>m.type!=="noclass" && m.date>=a && m.date<=b);}
function lecByN(n){return SCHED.find(m=>m.n===n);}
function confLabel(v){return v>=0.75?"comfortable":v>=0.55?"mostly there":"needs work";}
function activeMisc(f,asOf){return f.since<=asOf && (!f.resolved || asOf<f.resolved);}
function badgeFor(t){return t==="you"?'<span class="badge b-you">YOUR NOTES</span>':t==="note"?'<span class="badge b-note">NOTE</span>':t==="syllabus"?'<span class="badge b-syllabus">SYLLABUS</span>':'<span class="badge b-slide">LECTURE</span>';}
const state={ tab:"debrief", ref:TODAY };
/* ---------- coverage: how filled-in the course is (mechanical count; grows as materials are added) ---------- */
function coverage(){
const lecs=SCHED.filter(m=>m.type==="lecture"), N=lecs.length;
if(!N) return { N:0, pct:0, missTopics:0, missReads:0, empty:true };
const knownTopics=lecs.filter(m=>m.title && String(m.title).trim()).length;
const expReadings=lecs.filter(m=>m.reading || m.readingTBD).length;
const knownReadings=lecs.filter(m=>m.reading).length;
const denom=N+expReadings, num=knownTopics+knownReadings;
return { N, pct: denom?Math.round(num/denom*100):100, missTopics:N-knownTopics, missReads:expReadings-knownReadings };
}
function renderSetup(){
const el=document.getElementById("setup"); if(!el) return;
const c=coverage(); let note;
if(c.empty){ note="Add your syllabus to the Syllabus & Info folder to build your dashboard — then add slides, readings, and notes as you go, and this bar fills up."; }
else if(c.missTopics===0 && c.missReads===0){ note="All sessions detailed. I'll keep refining as you add notes, slides, and feedback."; }
else {
const miss=[];
if(c.missTopics) miss.push(c.missTopics+" lecture topic"+(c.missTopics>1?"s":""));
if(c.missReads) miss.push(c.missReads+" reading"+(c.missReads>1?"s":""));
note="This dashboard fills in as you add materials — still waiting on "+miss.join(" and ")+". Add them (or drop in slides) and the weekly views build out.";
}
el.innerHTML=`<div class="setrow"><span class="setlbl">Dashboard coverage</span><div class="setbar"><span style="width:${c.pct}%"></span></div><span class="setpct">${c.pct}% built</span></div><div class="setnote">${esc(note)}</div>`;
}
function renderOnboard(){
return `<div class="callout"><div class="ctag">Almost there</div><p>Your dashboard shell is ready. Add your <b>syllabus</b> to the <b>Syllabus & Info</b> folder and tell me — I'll read it and build out your schedule, deadlines, and coaching. Then drop slides, readings, and notes into their folders as you get them, and this fills in.</p></div>
<div class="view"><p class="sectlbl">What this becomes</p>
<div class="cov"><span class="datepill dp">DEBRIEF</span><div><div class="covttl">Weekly Debrief</div><div class="covmeta">A look back at each week — your notes recapped and where you're shaky.</div></div></div>
<div class="cov"><span class="datepill dp">UPCOMING</span><div><div class="covttl">Upcoming Week</div><div class="covmeta">Your next classes: readings, deadlines, and prep so you walk in ready.</div></div></div>
<div class="cov"><span class="datepill dp">LONG-TERM</span><div><div class="covttl">Long-Term Success</div><div class="covmeta">Exams and papers tracked from today, with what to do now.</div></div></div></div>`;
}
/* ---------- your notes for a lecture: detailed, cleaned-up bullets (array) or a paragraph (string) ---------- */
function renderNotes(n){
const nn=V.notes && V.notes[n]; if(!nn) return "";
let body;
if(Array.isArray(nn)){
body=`<ul class="ynlist">${nn.map(p=>{const f=/^(flag|flagged|watch out|open question)/i.test(p);return `<li class="${f?"flag":""}">${esc(p)}</li>`;}).join("")}</ul>`;
} else { body=`<div class="ynp">${esc(nn)}</div>`; }
return `<div class="yournotes"><div class="ynhead"><span class="badge b-you">YOUR NOTES</span> cleaned up from your notes</div>${body}</div>`;
}
/* ---------- WEEKLY DEBRIEF (reflects the week just finished) ---------- */
function renderDebrief(){
const ref=parseD(state.ref);
const wkMon=iso(addDays(ref,-6));
const met=meetingsBetween(wkMon,iso(addDays(ref,-1)));
const coach=((V.coach&&V.coach.debrief)||{})[wkMon];
const dv=(V.debrief||{})[wkMon];
let h="";
if(coach){ h+=`<div class="callout"><div class="ctag">Let's debrief</div><p>${esc(coach)}</p></div>`; }
else if(met.length){ h+=`<div class="callout plain"><div class="ctag">Let's debrief</div><p>Here's the week of ${esc(fmt(parseD(wkMon)))}. Tell me how it went and I'll write your coaching read.</p></div>`; }
if(!met.length){
h+=`<div class="view"><div class="emptyv">No class meetings the week of ${esc(fmt(parseD(wkMon)))}. Use the arrows to review an earlier week.</div></div>`;
return h+coachHint("debrief");
}
h+=`<div class="view"><p class="sectlbl">What this week covered</p>`;
met.forEach(m=>{ const d=parseD(m.date);
if(m.type==="exam"){ h+=`<div class="cov"><span class="datepill exam dp">${fmtShort(d)}</span><div><div class="covttl">${esc(m.title)}</div><div class="covmeta">In-class exam covering Lectures ${m.covers.join(", ")}.</div></div></div>`; }
else { h+=`<div class="cov"><span class="datepill dp">${fmtShort(d)}</span><div><div class="covttl">Lecture ${m.n} — ${esc(m.title)}</div><div class="covmeta">${esc(m.reading?m.reading.gist:m.q)}${m.reading?` <span style="color:var(--mut)">Reading: ${esc(m.reading.cite)}.</span>`:""}</div>${renderNotes(m.n)}</div></div>`; }
});
h+=`</div>`;
if(dv && dv.questions && dv.questions.length){
h+=`<div class="view"><p class="sectlbl">Reflect with your coach <span class="soft">— reply in the chat</span></p>`;
dv.questions.forEach(q=>{ h+=`<div class="qq">${esc(q)}</div>`; });
h+=`<div class="hint" style="margin-top:12px;">Tell me how these landed — e.g. <em>“that finally clicked”</em> or <em>“I'm still lost on the second reading”</em> — and I'll update your learning model and this dashboard.</div></div>`;
}
if(dv && dv.noticing && dv.noticing.length){
h+=`<div class="view"><p class="sectlbl">What I'm noticing <span class="soft">— from your notes and our chats</span></p>`;
dv.noticing.forEach(n=>{ h+=`<div class="notice ${n.type==="good"?"good":""}">${esc(n.text)}</div>`; });
h+=`</div>`;
}
return h+coachHint("debrief");
}
/* ---------- UPCOMING WEEK (next meetings) ---------- */
function renderUpcoming(){
const ref=parseD(state.ref);
const upMon=iso(addDays(ref,1));
const met=meetingsBetween(upMon,iso(addDays(ref,7)));
const coach=((V.coach&&V.coach.upcoming)||{})[upMon];
const isPast=state.ref<TODAY;
let h="";
if(coach){ h+=`<div class="callout${isPast?' plain':''}"><div class="ctag">${isPast?"What you were looking at then":"Coach's read"}</div><p>${esc(coach)}</p></div>`; }
else if(met.length){ h+=`<div class="callout plain"><div class="ctag">The week ahead</div><p>Week of ${esc(fmt(parseD(upMon)))} — ${met.length} meeting${met.length>1?"s":""}.</p></div>`; }
if(!met.length){ return `<div class="view"><div class="emptyv">No class meetings the week of ${esc(fmt(parseD(upMon)))}.</div></div>`+coachHint("upcoming"); }
met.forEach(m=>{ const d=parseD(m.date);
if(m.type==="exam"){
h+=`<div class="view"><div class="lhead"><span class="datepill exam">${fmtShort(d)}</span><span class="ltitle">${esc(m.title)} — in class</span></div>
<div class="readrow"><span class="badge b-exam">EXAM</span> Covers Lectures ${m.covers.join(", ")}</div>
<div class="msub">Review before ${DOW[d.getDay()]}</div><ul class="q">
${m.covers.map(n=>{const L=lecByN(n);return L?`<li>Lecture ${n} — ${esc(L.title)}</li>`:"";}).join("")}</ul>
<div class="hint" style="margin-top:11px;">Ask me to <em>run a practice quiz</em> or <em>help you prep</em> in the chat.</div></div>`;
} else {
const uc=(V.upcoming||{})[m.n]||{};
const del=uc.deliverable; // course-specific pre-class work, if any (label + draft + due). Omit for courses with none.
h+=`<div class="view"><div class="lhead"><span class="datepill">${fmtShort(d)}</span><span class="ltitle">Lecture ${m.n}${m.title?" — "+esc(m.title):" — topic TBA"}</span></div>`;
h+=`<div class="readrow">`;
if(m.reading){ h+=`<span class="badge b-read">READING</span> ${esc(m.reading.cite)}`; }
else if(m.readingTBD){ h+=`<span style="color:var(--mut)">Reading to be posted — check back closer to class.</span>`; }
else if(m.title){ h+=`<span style="color:var(--mut)">No assigned reading.</span>`; }
else { h+=`<span style="color:var(--mut)">Details to be announced — add materials when released.</span>`; }
if(del && del.due){ h+=` <span class="due">${esc(del.label||"Due")}: ${esc(del.due)}</span>`; }
h+=`</div>`;
const left=[], right=[];
if(m.reading){
left.push(`<div class="msub">The gist</div><div class="prose">${esc(m.reading.gist)}</div><div class="prov">From the reading · plus general context</div>`);
if(m.reading.theme) left.push(`<div class="msub">Why it matters here</div><div class="prose">${esc(m.reading.theme)}</div>`);
}
if(uc.prep) left.push(`<div class="msub">Come prepared</div><div class="prose">${esc(uc.prep)}</div>`);
if(del && del.draft) right.push(`<div class="msub">${esc(del.label||"Draft to submit")}</div><div class="draft"><div class="ex">${esc(del.draft)}</div></div>`);
if(uc.questions && uc.questions.length) right.push(`<div class="msub">Bring to class</div><ul class="q">${uc.questions.map(q=>`<li>${esc(q)}</li>`).join("")}</ul>`);
if(left.length || right.length){ h+=`<div class="split"><div>${left.join("")}</div><div>${right.join("")}</div></div>`; }
else { h+=`<div class="emptyv">No prep details yet — add the reading or slides for this lecture and I'll fill this in.</div>`; }
if(uc.diagram){ h+=`<div class="msub">The board, cleaned up</div><div class="diagram">${uc.diagram}</div>`; }
h+=`</div>`;
}
});
return h+coachHint("upcoming");
}
/* ---------- LONG-TERM SUCCESS (as of TODAY) ---------- */
function renderLongterm(){
let h="";
const finals=ASSESS.filter(a=>a.date>TODAY).sort((a,b)=>a.date<b.date?-1:1);
const nextExam=finals[0];
const coach=(V.coach&&V.coach.longterm)||"";
if(coach) h+=`<div class="callout"><div class="ctag">Coach's read</div><p>${esc(coach)}</p></div>`;
if(finals.length){
h+=`<div class="mgrid">`;
finals.forEach((m,i)=>{ const days=daysBetween(TODAY,m.date), soon=i===0;
h+=`<div class="mile"><div class="mtop"><span class="mname">${esc(m.title)}</span><span class="mcount${soon?"":" soft"}">in ${days} days</span></div>
<div class="mmeta">${esc(fmt(parseD(m.date)))}${m.time?" · "+esc(m.time):""}${m.weight?" · "+m.weight+"%":""} · ${Array.isArray(m.covers)?"Lectures "+m.covers.join("–"):esc(m.covers||"")}</div></div>`;
});
h+=`</div>`;
}
if(nextExam && Array.isArray(nextExam.covers)){
h+=`<div class="view"><p class="sectlbl">${esc(nextExam.title)} readiness by topic</p>`;
nextExam.covers.forEach(n=>{ const L=lecByN(n), covered=L&&L.date<=TODAY, c=CONCEPTS.find(x=>x.lec===n);
if(covered&&c){ const v=c.conf, pct=Math.round(v*100), col=v>=0.6?"var(--life)":"var(--med)", label=confLabel(v), st=v>=0.55?"st-ok":"st-work";
h+=`<div class="crow"><div class="cinfo"><div class="cname"><span class="lx">LEC ${n}</span>${esc(L.title)}</div>
<div class="bar"><span style="width:${pct}%;background:${col}"></span></div><div class="cnote">${esc(c.note)}</div></div><div class="status ${st}">${label}</div></div>`;
} else if(L){ const rel=daysBetween(TODAY,L.date);
h+=`<div class="crow up"><div class="cinfo"><div class="cname"><span class="lx">LEC ${n}</span>${esc(L.title)}</div>
<div class="bar"><span style="width:0%"></span></div><div class="cnote">Covered ${esc(fmt(parseD(L.date)))} — I'll assess after the lecture.</div></div><div class="status st-soon">${rel<=7?"this week":"upcoming"}</div></div>`;
}
});
h+=`</div>`;
}
const lt=V.longterm||{};
if(lt.actions && lt.actions.length){
h+=`<div class="view"><p class="sectlbl">Do this now</p>`;
lt.actions.forEach(a=>{ h+=`<div class="action"><div class="at">${esc(a.title)}</div><div class="am">${esc(a.body)}</div>${(a.src&&a.src.length)?`<div style="margin-top:7px;">${a.src.map(badgeFor).join(" ")}</div>`:""}</div>`; });
if(lt.examStrategy) h+=`<div class="hint" style="margin-top:11px;"><div class="ct">Strategy</div>${esc(lt.examStrategy)}</div>`;
h+=`</div>`;
}
return h+coachHint("longterm");
}
function coachHint(tab){
const tips={
debrief:["that finally clicked","I'm still shaky on this week's material","quiz me on this week"],
upcoming:["I finished this week's reading","help me prep for this class","what's due this week?"],
longterm:["build me a study plan","help me start my notes sheet","quiz me on what I've covered"]
}[tab];
return `<div class="hint"><div class="ct">Talk to your coach</div>This dashboard updates when you tell me things. Try: ${tips.map(t=>"<em>“"+esc(t)+"”</em>").join(" · ")}.</div>`;
}
/* ---------- nav + tabs (ephemeral view controls only) ---------- */
function renderNav(){
const nav=document.getElementById("weeknav");
if(state.tab==="longterm"){ nav.style.display="none"; return; }
nav.style.display="flex";
const ref=parseD(state.ref), isPast=state.ref<TODAY, isFut=state.ref>TODAY;
const mon=state.tab==="debrief"?addDays(ref,-6):addDays(ref,1);
const label=(state.tab==="debrief"?"Debrief — week of ":"Week of ")+fmt(mon);
const prevOK=iso(addDays(ref,-7))>=TERM_START, nextOK=iso(addDays(ref,7))<=TERM_END;
const flag=isPast?'<span class="pastflag">Past week · snapshot</span>':isFut?'<span class="futflag">Future · preview</span>':'';
nav.innerHTML=`<button class="navbtn" data-act="prev" ${prevOK?"":"disabled"} aria-label="Previous week">‹</button>
<span class="wklabel">${label}</span>${flag}
<button class="navbtn" data-act="next" ${nextOK?"":"disabled"} aria-label="Next week">›</button>
${state.ref!==TODAY?`<button class="backnow" data-act="now">Back to this week →</button>`:""}`;
}
function shiftWeek(dir){ state.ref=iso(addDays(parseD(state.ref),7*dir)); render(); }
function setTab(t){ state.tab=t; document.querySelectorAll(".tab").forEach(b=>b.classList.toggle("active",b.dataset.tab===t)); render(); }
document.getElementById("tabs").addEventListener("click",e=>{ const b=e.target.closest(".tab"); if(b) setTab(b.dataset.tab); });
document.addEventListener("click",e=>{
const a=e.target.closest("[data-act]"); if(!a) return;
if(a.dataset.act==="prev") shiftWeek(-1);
else if(a.dataset.act==="next") shiftWeek(1);
else if(a.dataset.act==="now"){ state.ref=TODAY; render(); }
});
function render(){
document.getElementById("ctitle").textContent=COURSE.title||"Course dashboard";
document.getElementById("sub").textContent=(COURSE.code?COURSE.code+" · ":"")+(COURSE.term||"")+(COURSE.meets?" · meets "+COURSE.meets:"");
renderSetup();
if(!SCHED.length){ document.getElementById("weeknav").style.display="none"; document.getElementById("view").innerHTML=renderOnboard(); return; }
renderNav();
const v=document.getElementById("view");
v.innerHTML=state.tab==="debrief"?renderDebrief():state.tab==="upcoming"?renderUpcoming():renderLongterm();
}
render();
</script>
</body>
</html>
```