Owner round (2026-10-06): markdown export of sessions, actioned from
the sidebar context menu — "Export as Markdown…" sits with the
management actions, above Delete (the destructive action stays last).
- GET /api/chats/:id/markdown (404 unknown): the whole session as one
attachment — text/markdown; charset=utf-8, filename from the slugified
title ("relational-databases-joins-normal-forms.md", non-ascii titles
fall back to "session.md"). Both session kinds export.
- The client is a same-origin anchor download; the route's
Content-Disposition names the file.
- sessionMarkdown's user-facing debut — minted ANCHORLESS (message-id
anchors are extraction provenance, not learner-facing). The engine
joins the core barrel (pure, browser-safe) for its first consumer.
The practice join (owner-approved mid-round): practice tool results are
bare acks — statement, outcome, verdict, and the model solution all
live in the assistant's call arguments. The engine now folds them the
same way the view seam folds cards: Problem blocks render the pose's
statement (solution never leaks at pose time); Concluded blocks render
the outcome as prose ("Solved with hints.") + verdict, then the
pose-committed model solution as a blockquoted reveal via
problemInputForConclude (clean-poses-only, its refusal law). A
dismissed problem concludes as "The learner skipped this problem." —
no reveal. Errored poses render nothing (the failed-tool law).
1427 tests (route pins: headers, slug, anchorless body, 404; engine
pins: pose-time statement, the reveal, the dismissal refusal, the
errored-pose silence); tsc, lint, build green.
38 lines
1.5 KiB
TypeScript
38 lines
1.5 KiB
TypeScript
/**
|
|
* THE SESSION MARKDOWN EXPORT (owner, release polish 2026-10-06): the
|
|
* user-facing export the session-markdown engine was built for — the
|
|
* same document the extractor reads, minted anchorless (anchors are
|
|
* extraction provenance; a learner's export carries no message ids).
|
|
*
|
|
* GET /api/chats/:id/markdown → the whole session as one markdown file
|
|
* (Content-Disposition attachment; filename from the slugified title).
|
|
* 404 unknown chat. Both session kinds export — practice sessions ride
|
|
* the same engine (problem/conclude results render as tool blocks,
|
|
* markers are invisible by the engine's law).
|
|
*/
|
|
import { sessionMarkdown } from "@/src/core";
|
|
import { getChat } from "@/src/server";
|
|
|
|
/** Filename-safe title (own module scope on purpose: tiny + pure). */
|
|
function slugify(title: string): string {
|
|
const slug = title
|
|
.toLowerCase()
|
|
.normalize("NFKD")
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
return slug.length > 0 ? slug : "session";
|
|
}
|
|
|
|
export async function GET(_req: Request, ctx: { params: Promise<{ id: string }> }): Promise<Response> {
|
|
const { id } = await ctx.params;
|
|
const record = getChat(id);
|
|
if (!record) return Response.json({ error: "Unknown chat" }, { status: 404 });
|
|
const markdown = sessionMarkdown(record.messages, { title: record.title, anchors: false });
|
|
return new Response(markdown, {
|
|
headers: {
|
|
"content-type": "text/markdown; charset=utf-8",
|
|
"content-disposition": `attachment; filename="${slugify(record.title)}.md"`,
|
|
},
|
|
});
|
|
}
|