feat(lessons): a lesson is readable, writable and browsable by a human (#3734)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Failing after 31s
CI & Build / integration (push) Successful in 48s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Failing after 31s
CI & Build / integration (push) Successful in 48s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Skipped
Step 7's actual UI. Before this the frontend had zero lesson code — the kind existed for agents only, which is rule 27 failing. THE EDITOR ASKS FOR THE TRIGGER BY NAME, and leads with it. Three fields — the trigger, the claim, the detail — never one markdown box. That is the design step 1 settled, and the evidence is blunt: the snippet corpus carries a trigger on every record with no guard anywhere, because a service composes the title from a named parameter. What is at 100% is a named structured field, not a writer remembering a convention. The trigger gets the most room, its own explanation, and a save button that refuses without it and says why. The form shows the composed title live, so the writer is agreeing to a document they can read rather than one assembled out of sight. A 409 from the duplicate gate is rendered as the record that already covers the moment, with a link to improve it and an explicit override — not as a failure. THE BROWSE VOCABULARY GAINS THE KIND, which #3161 warned this step not to get wrong: a facet chip, a badge label, and routing to `/lessons/:id` rather than the note editor, which cannot edit a trigger. The badge is neutral alongside snippet and process — a hue would make the softest record in the corpus look like the loudest, next to a rule that actually binds. BOTH DIRECTIONS OF THE PROVENANCE. The detail page resolves `learned_from` to titles rather than bare ids, because "#4181" tells a reader nothing about whether it is worth opening. And `LessonsTaughtPanel` answers the reverse on the record's own page — the direction the task body calls the one that gets forgotten. It has no author to type it, which is exactly why it tends never to get built. A component, not markup in the task editor, so the same panel mounts on any record a lesson can cite instead of being written a second time (#3207). Silent when empty: most records taught no lesson, and a panel that says "None yet" everywhere is one people learn to skip. GLOBAL-BY-DEFAULT IS MADE LEGIBLE. A lesson meeting you on a project it was not written on reads as a bug unless the page says otherwise, so the origin line says it as a property of the kind rather than as an apology. Design system tokens throughout; no new raw hex. `--fs-error` rather than `--fs-danger` — 31 uses against 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* "What was learned from this record" — the reverse of a lesson's
|
||||
* `learned_from`.
|
||||
*
|
||||
* THE DIRECTION THAT GETS FORGOTTEN, and arguably the more useful one. The
|
||||
* forward link is easy to remember because the lesson's author types it; this
|
||||
* one has no author and so tends never to get built. A reader opening an old
|
||||
* issue wants to know what came out of it, and without this the relation is
|
||||
* navigable only from the lesson's side.
|
||||
*
|
||||
* A COMPONENT rather than markup inside the task editor, because the same
|
||||
* question is worth answering on any record a lesson can cite — an issue, a
|
||||
* spike, a dev-log. One panel, mounted wherever that is true, instead of the
|
||||
* shape being written a second time the first time someone wants it on notes
|
||||
* (#3207).
|
||||
*
|
||||
* SILENT WHEN EMPTY. Most records taught no lesson, and a panel that renders
|
||||
* "None yet" on every page is a panel people learn to skip — which costs the
|
||||
* pages where it does have something to say.
|
||||
*/
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
|
||||
import { lessonsTaughtBy, type Lesson } from "@/api/lessons";
|
||||
|
||||
const props = defineProps<{ recordId: number }>();
|
||||
|
||||
const lessons = ref<Lesson[]>([]);
|
||||
// No error surface on purpose: this is a secondary panel beside the record the
|
||||
// reader actually came for, and a red box about a failed side-query would be
|
||||
// louder than the thing it failed to fetch. It stays silent and stays absent.
|
||||
const loaded = ref(false);
|
||||
|
||||
async function load() {
|
||||
loaded.value = false;
|
||||
lessons.value = [];
|
||||
if (!props.recordId) return;
|
||||
try {
|
||||
const res = await lessonsTaughtBy(props.recordId);
|
||||
lessons.value = res.lessons;
|
||||
} catch {
|
||||
lessons.value = [];
|
||||
} finally {
|
||||
loaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.recordId, load);
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="loaded && lessons.length" class="ltp">
|
||||
<h3 class="ltp-label">What was learned from this</h3>
|
||||
<ul class="ltp-list">
|
||||
<li v-for="l in lessons" :key="l.id" class="ltp-item">
|
||||
<router-link :to="`/lessons/${l.id}`" class="ltp-link">
|
||||
{{ l.what || l.title }}
|
||||
</router-link>
|
||||
<!-- The trigger travels with the row. A lesson listed without it is a
|
||||
claim with the half that says when it matters left off. -->
|
||||
<p v-if="l.when_to_apply" class="ltp-trigger">
|
||||
{{ l.when_to_apply }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ltp {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--fs-border-color);
|
||||
}
|
||||
.ltp-label {
|
||||
margin: 0 0 0.6rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--fs-text-tertiary);
|
||||
}
|
||||
.ltp-list { list-style: none; margin: 0; padding: 0; }
|
||||
.ltp-item { margin-bottom: 0.7rem; }
|
||||
.ltp-link { color: var(--fs-accent); font-size: 0.9rem; }
|
||||
.ltp-trigger {
|
||||
margin: 0.15rem 0 0;
|
||||
color: var(--fs-text-secondary);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user