---
path: /metis/byom
surface: customer
domain: metis
auth: signed-in
source: apps/oshun/web/src/app/metis/byom/page.tsx
status: walked
last_walked:
  '2026-06-30 by Codex (standalone PWA alias launch verified the BYOM mobile
  stacked material table and no horizontal page overflow in
  metis-upload-alias.spec.ts), against commit HEAD'
  '2026-06-30 by Codex (BYOM Browse/Paste/drop controls wired to POST /v1/metis/ingest and covered against real dev infra in metis-byom-ingest.spec.ts), against commit HEAD'
  '2026-06-25 by Codex (BYOM material table, drop zone, guide preview, outline, provenance, and action anchors covered in metis-byom-ingest.spec.ts), against commit HEAD'
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; live
  screen-reader, touch, offline, and telemetry-delivery checks pending a manual
  AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content
  re-verified 2026-06-03 against current source'
---

# Metis · BYOM (bring your own materials)

## Purpose

Bring your own materials. Upload books, PDFs, notes, and URLs; Metis parses them
and generates a study guide grounded in those sources. Fixture lists four
materials with parse status.

## Entry points

- **Metis schoolroom** — verify cross-link from [`metis.md`](./metis.md)
- **Direct URL / bookmark** — yes

## Layout regions

`page.tsx` is a thin server component: `await getMetisByom()` →
`<MetisBYOM data={data} />`.

Inside `MetisBYOM` (`metis.tsx`), the root carries `data-metis-byom-page=""`,
`data-metis-byom-material-count="{N}"`, and
`data-metis-byom-outline-count="{N}"`:

- **Web shell top** — `LCustomerNav active="library"`
- **Header row** (`data-metis-byom-header`, flex row, stacked below 900 px):
  - **Left — title** — `data-metis-byom-eyebrow` text "Metis · BYOM · bring your
    own materials"; `data-metis-byom-heading` headline "From your books, your
    study guide."
  - **Right — Generate guide →** (`data-metis-byom-generate-cta`,
    `LBtn kind="primary"`)
- **Two-column body** (`gridTemplateColumns: '1.3fr 1fr'`; stacked to one column
  below 900 px):
  - **Left — Materials table** — `LEyebrow` "Your materials · <count>"; table
    grid carries `data-metis-byom-materials-table=""` and
    `data-metis-byom-materials-count="{N}"`, with named column anchors
    `kind/title/pages/rights/status`. Each material row carries
    `data-metis-byom-material-row=""`, row index, kind, title, pages, rights,
    and status attributes; visible cells carry `data-metis-byom-material-cell`
    for `kind`, `title`, `pages`, `rights`, and `status`
    (`LValidationPill kind={s === 'parsed' ? 'ok' : 'info'}`). Below 640 px,
    route-scoped responsive rules hide the desktop column header and reflow each
    material row into a compact kind/status, title, pages/rights grid so the
    page has no global horizontal overflow.
  - **Left — Drop zone** — dashed-border drop card "Drop a book, a PDF, your
    notes." with `data-metis-byom-drop-zone=""`, constraint copy anchored at
    `data-metis-byom-drop-zone-constraints`, and a client island
    (`data-metis-byom-ingest-controls`) wrapping live Browse files / Paste URL
    controls plus drag-and-drop. Markdown/text bytes post as `kind=text` and
    render a completed outline; URL/PDF references post as `kind=url` /
    `kind=pdf` and render the BFF's `awaiting_source_content` state.
  - **Right column** — generated study-guide preview with
    `data-metis-byom-guide-preview=""`, source count, outline count, guide meta,
    guide heading, outline row/cell anchors, provenance card/copy anchors, and
    guide action wrappers for `edit-outline` and `begin-week-1`.

## States

- [x] **Loaded** — `getMetisByom()` resolves before render; Playwright verifies
      `[data-metis-byom-page]` and material/outline counts.
- [x] **Materials present** — table rows for each `data.materials` entry are
      verified row-by-row through row attributes and cell anchors.
- [ ] **Materials empty** — verify the empty-state copy (the drop zone is still
      visible)
- [x] **Parse status: parsed** — `LValidationPill kind="ok"` visible for the
      three parsed rows; row status attributes and cell text are asserted.
- [x] **Parse status: queued** — `LValidationPill kind="info"` visible for the
      queued row; row status attribute and cell text are asserted.
- [x] **Guide preview present** — generated-guide meta, heading, all five
      outline rows, provenance heading/copy, and guide action wrappers are
      asserted. The CTA remains display-only.
- [x] **BYOM URL source accepted** — Paste URL posts to `POST /v1/metis/ingest`,
      then the page renders `data-metis-byom-ingest-status` with
      `data-state="awaiting_source_content"`, `data-source-kind="url"`, and the
      deploy-bound extraction reason.
- [x] **BYOM Markdown/text file completed** — Browse files and drag-and-drop
      read Markdown/text bytes in the browser, post `kind=text`, and render
      `data-state="completed"` with outline summary attributes.
- [ ] **Generate guide post-click** — verify destination once the CTA is wired
      (in-page guide vs. navigation)
- [ ] **Upload over quota** — verify what happens at 50 MB per file / 200 MB
      total
- [x] **Rights chip** — `LChip` per material's rights category; fixture values
      `owned`, `mine`, `fair use`, and `cc-by` are covered.
- [x] **Standalone PWA / mobile layout** — launch through `/metis/upload` lands
      on BYOM in standalone mode with query params intact; the mobile material
      table uses the compact row layout and the page has no horizontal overflow.

## Interactions

### Header

- [x] **Generate guide →** (`data-metis-byom-generate-cta`,
      `LBtn     kind="primary"`)
  - Function: display-only in the current specimen; Playwright verifies presence
    and deliberately does not click until the destination is wired.

### Materials table

- [x] **Row (per material)** — non-link, non-button row; Playwright verifies all
      four fixture rows by index, kind/title/pages/rights/status attributes, and
      visible cell text. No view/remove/re-parse controls exist in source.
- [x] **Status pill** — non-interactive; parsed/queued cell text covered.
- [x] **Rights chip** — non-interactive; owned/mine/fair use/cc-by cell text
      covered.

### Drop zone

- [x] **Drag-and-drop card** — `data-metis-byom-drop-zone` handles dropped
      Markdown/text files through `MetisByomIngestControls`, posts the real BFF
      ingest route, and renders the completed outline state. Playwright
      dispatches a browser `DataTransfer` with `stoic-drop.md` against real dev
      infra.
- [x] **Browse files** (`data-metis-byom-upload-action="browse-files"`,
      `LBtn     kind="ghost"`)
  - Function: opens the hidden `data-metis-byom-file-input`. Markdown/text files
    are read as bytes and submitted as `kind=text`; PDF files submit a persisted
    `kind=pdf` source reference without fabricated parsing.
- [x] **Paste URL** (`data-metis-byom-upload-action="paste-url"`,
      `LBtn     kind="ghost"`)
  - Function: opens `data-metis-byom-url-form`, posts the URL as `kind=url`, and
    renders the BFF's `awaiting_source_content` response.

## Data & contracts

- **Reads**: `getMetisByom()` from `@/lib/lilith-data/metis-depth` — returns
  `MetisByomData` (verify shape: `materials: { ic, t, p, r, s }[]` and the
  generated guide payload)
- **Writes**: `POST /v1/metis/ingest` from the BYOM upload island. Markdown/text
  files are browser-read and submitted as `kind=text`; URL/PDF references are
  persisted honestly as `awaiting_source_content`. Live course proposal writes
  remain covered by `metis-ingest-bff.spec.ts` and the journey spec.
- **Realtime**: _None observed in this view._
- **Caching**: server fetch
- **Auth/role check**: shell middleware

## Cross-references

- Sibling Metis routes:
  - [`metis.md`](./metis.md), [`metis-assessment.md`](./metis-assessment.md),
    [`metis-lesson.md`](./metis-lesson.md), [`metis-tutor.md`](./metis-tutor.md)
- Cross-domain partners: Nisaba (`../07-nisaba/`) for source-grounded citations
  once the guide is generated
- Component sources:
  - `apps/oshun/web/src/components/lilith/metis.tsx` (`MetisBYOM`)
- E2E:
  [`apps/oshun/web/e2e/metis-byom-ingest.spec.ts`](../../../apps/oshun/web/e2e/metis-byom-ingest.spec.ts)
  covers the header, material table columns/counts, every material row/cell,
  drop-zone constraints, live Browse/Paste/drop ingest controls against real dev
  infra, guide preview counts, outline rows/cells, provenance, guide action
  wrappers, and the operator Metis dashboard.
  [`apps/oshun/web/e2e/metis-upload-alias.spec.ts`](../../../apps/oshun/web/e2e/metis-upload-alias.spec.ts)
  covers the legacy alias landing on this page in standalone PWA mode, including
  the mobile no-horizontal-overflow contract.
- Feature spec: [`V1/features.md#metis`](../../../V1/features.md#metis)

## Open questions / known gaps

- [x] Confirm whether drag-and-drop and the Browse files / Paste URL controls
      are wired in V1 — they now call the live `POST /v1/metis/ingest` route
      from this page, with Playwright coverage for URL, browse, and drop.
- [ ] Wire PDF byte extraction and EPUB/DOCX parsing if those formats are meant
      to produce outlines from this page instead of persisted source-reference
      jobs.
- [ ] Document the "Generate guide" destination once it is wired — does it open
      `/metis/lesson` with a generated payload, or expand the guide in-place?
- [ ] Extend the parse-status state machine beyond the current fixture values
      (`parsed`, `queued`) when parsing/failed rows are available.
