Skip to content

Artifacts: the federated library

Everything Kwilo generates for a user — question banks, lesson plans, presentations, research reports, practice tests, and (for B2C) generated images — surfaces on one read-only browse page: /artifacts on web, the Artifacts screen on mobile B2C. Domain tables stay the source of truth; the page is a federation, not a store. Viewing is never quota-metered. Related: Practice tests as Artifacts, Chat image generation, Chat Workspace (where artifacts are born).

Retroactive doc: Phase 1 shipped in MySetu-AI/kwiloai_webapp #1240 without a doc; authored now alongside the B2C visibility/images change, per the retrofit rule.

Why this exists

  1. Who asked — product owner. Users create things in chat and lose them: the only way back was scrolling chat history.
  2. User pain — "I made a presentation last week in chat and can't find it." Generated outputs were scattered across five tables with no unified list.
  3. Cost of not doing it — generated content is effectively disposable; repeat generation burns quota and erodes the "Kwilo builds a library for you" value story.
  4. Validated or a guess — grounded in chat-history scroll behavior; re-open rate is the watch metric (phases 2–3 are gated on it).
  5. How we'll know it worked — users re-open artifacts from the page instead of re-generating or scrolling chat.

How it works

GET /artifacts merges owner-scoped rows from the typed generation stores into one paginated, searchable, type-filterable list. Two design decisions carry the feature:

  • Decision 1 — doc-shaped types share one store and one viewer. question_bank, lesson_plan, assignment, module live in documents (discriminated by doc_type) and share the read-only viewer (teachers get the editor instead).
  • Decision 2 — federation, not duplication. Domain tables (documents, presentations, deep_research_sessions, mock_tests, practice_sessions + attempts, media_generations) remain canonical; the service only reads and merges. Queries are always scoped to the requesting user — never the org unit — because all B2C users share the synthetic "Individual Kwilo" org and org scoping there would expose strangers' content.

Type → backing store → open action

Type Store Open action (web)
question_bank / lesson_plan / assignment / module documents Teacher → document editor; others → read-only dialog
presentation presentations Presentation viewer
research_report deep_research_sessions Chat research viewer (/new-chat?research=:id)
practice_test (source: "mock_test") mock_tests Disabled (no reopenable route yet)
practice_test (source: "practice_session") practice_sessions Review screen /practice/test/:id
image (B2C only) media_generations Lightbox (web) / detail screen (mobile)

Delete is owner-checked per store; the URL's type must match the row (/artifacts/question_bank/{id} cannot delete a lesson plan). Image delete also removes the GCS object (best effort).

Role visibility (2026-07-12)

Which types federate depends on the caller's role — enforced server-side (visible_artifact_types_for_role), so a hand-crafted ?type= deep link can't resurface a hidden type, and mirrored in the clients' tab/chip lists:

  • B2B roles (instructor, learner, admins…): the Phase-1 seven types. No images yet — teachers' generated images have their own surfaces, and widening B2B artifacts is a separate decision.
  • B2C learners (b2c_user): everything except question_bank and research_report, plus image.
  • Question banks are educator-shaped; a B2C learner can't create one, so the tab was permanently-empty noise.
  • Research reports stay on the chat/research surface where B2C users actually run and reopen research.
  • Images are the chat Image-chip outputs (a metered pool, Pro-unlimited); before this they vanished once the chat scrolled away — no gallery.

Counts follow visibility: the response's counts map only carries visible types, so the "All" tab count never leaks hidden rows.

Images

Image artifacts federate from media_generations rows with media_type="image", status="completed", and a storage_path (chat-relay persistence writes these — see Chat image generation). The list response carries a short-lived (1 h) signed GCS URL per item, minted/refreshed at list time; the card shows a thumbnail, and open renders the full image — web in a lightbox dialog, mobile on the artifact detail screen (URL rides along as a route param, same pattern as other non-document types). The prompt doubles as the title (truncated to 120 chars).

Non-goals and rejected alternatives

  • An artifacts table (rejected). Federation keeps domain tables canonical; a mirror table would drift and double-write.
  • Hiding types client-side only (rejected). Tabs are client concern, but data visibility is enforced in the service — a deep link must not bypass the role gate.
  • Images for B2B roles (deferred, not rejected). The service supports it (one line in the role map); shipping it is a product decision about teacher image workflows, not a tech gap.
  • Editing from the artifacts page (out of scope). The page is read + delete only; editing happens on each type's own surface.
  • Server-side subject filter (out of scope). Subject filtering is client-side over the loaded page; the backend contract has no subject param.

Known gaps (deferred)

  • Mock-test-sourced practice cards are still unopenable (no reopenable route).
  • A B2C user's pre-existing research/question-bank rows are hidden, not deleted — they reappear if the role's visibility ever widens.
  • Signed image URLs expire after 1 h; a mobile detail screen left open longer than that shows a broken image until reopened from the list.

Changelog

  • 2026-07-12 — Role-based visibility + images. B2C learners no longer see question_bank / research_report; their generated chat images now federate as image artifacts with thumbnails and a lightbox/detail viewer (web + mobile B2C). Backend gate visible_artifact_types_for_role. MySetu-AI/kwiloai_webapp #1332.
  • 2026-07-04 → 07-10 — Practice-hub sessions joined the practice_test type with source discriminator (#1313); see Practice tests as Artifacts.
  • 2026-07-02 (retro) — Phase 1: /artifacts federation over four stores + chat capture (#1240). Phases 2–3 (pinning, richer viewers) gated on re-open rate.