// DocsSessionIndex.jsx — v0.21.0 (was v0.20.0 / roadmap item #30). // // Per-session landing at `/docs/sessions/:nnnn`. v0.20.0 rendered a // dead-end "N transcript(s) in this session. Select one from the // navigation." placeholder. v0.21.0 / roadmap item #32 collapses that: // the session root now renders a transcript INLINE so the URL is never // an empty stop. // // - Exactly one transcript → render it inline at the session root. // - Multiple transcripts → render the `.0` driver transcript // inline (fall back to the first file by // sort order if there's no `.0`), AND // list/link the remaining transcripts so // the siblings are one click away. // // The URL stays stable to the session number — this is an inline // render, not a 301/redirect. The per-transcript route // (`/docs/sessions/:nnnn/:filename`) still exists and is what the // sibling links and the left-nav transcript rows point at. // // The transcript count + filenames come from `/api/docs/sessions/:nnnn/index` // so the empty-state ("no transcripts yet"), not-found, and error // paths remain meaningful when the upstream is mid-publish or // unreachable. The metadata header + body rendering are imported from // DocsSessionTranscript.jsx so the inline view is byte-identical to the // standalone per-transcript view. import { useEffect, useState, useCallback } from 'react' import { Link, useParams } from 'react-router-dom' import MarkdownPreview from './MarkdownPreview.jsx' import { getSessionsManifest, getSessionIndex, getSessionTranscript, } from '../api.js' import { TranscriptMetaHeader, transcriptOrdinal, } from './DocsSessionTranscript.jsx' import { EVENTS, track } from '../lib/analytics' import './Docs.css' // Pick the transcript to render inline at the session root: prefer the // `.0` driver transcript; otherwise the first file by sort order. The // backend already returns the file list sorted, so `files[0]` is a // stable fallback. function pickPrimary(files) { if (!files || files.length === 0) return null const driver = files.find(f => /^SESSION-\d{4}\.0-TRANSCRIPT/.test(f)) return driver || files[0] } export default function DocsSessionIndex() { const { nnnn } = useParams() const [title, setTitle] = useState('') const [tldr, setTldr] = useState('') const [files, setFiles] = useState([]) const [status, setStatus] = useState('loading') // loading | ok | notfound | error const [reloadTick, setReloadTick] = useState(0) // The inline body for the primary transcript. const [body, setBody] = useState('') const [bodyStatus, setBodyStatus] = useState('idle') // idle | loading | ok | notfound | error useEffect(() => { track(EVENTS.DOC_VIEWED, { section: `sessions/${nnnn}` }) }, [nnnn]) // Title + optional TL;DR from the manifest — cheap, cached server-side. useEffect(() => { let active = true getSessionsManifest() .then(payload => { if (!active) return const entry = (payload && payload[nnnn]) || {} setTitle(entry.title || '') // `tldr` is an optional manifest field (string). Absent ⇒ the // header renders no TL;DR line (graceful degrade). setTldr(typeof entry.tldr === 'string' ? entry.tldr : '') }) .catch(() => { // Title + TL;DR are decorative; the transcript list + body // fetches below are the load-bearing ones. }) return () => { active = false } }, [nnnn]) // File list from the per-session index endpoint. useEffect(() => { let active = true setStatus('loading') getSessionIndex(nnnn) .then(payload => { if (!active) return setFiles((payload && payload.files) || []) setStatus('ok') }) .catch(e => { if (!active) return if (e.status === 404) { setStatus('notfound') } else { setStatus('error') } }) return () => { active = false } }, [nnnn, reloadTick]) const primary = status === 'ok' ? pickPrimary(files) : null // Fetch the primary transcript body once we know which file it is. useEffect(() => { if (!primary) { setBody('') setBodyStatus('idle') return } let active = true setBodyStatus('loading') getSessionTranscript(nnnn, primary) .then(text => { if (!active) return setBody(text || '') setBodyStatus('ok') }) .catch(e => { if (!active) return setBodyStatus(e.status === 404 ? 'notfound' : 'error') }) return () => { active = false } }, [nnnn, primary, reloadTick]) const retry = useCallback(() => setReloadTick(t => t + 1), []) const header = title ? `${nnnn} — ${title}` : `Session ${nnnn}` const siblings = primary ? files.filter(f => f !== primary) : [] return (

{header}

{status === 'loading' &&

Loading…

} {status === 'notfound' && (

No transcripts have been published for this session yet.{' '} About sessions

)} {status === 'error' && (

Couldn't reach the session-history repo.

)} {status === 'ok' && files.length === 0 && (

This session has no transcripts published.

)} {status === 'ok' && primary && ( <> {siblings.length > 0 && ( )} {bodyStatus === 'loading' &&

Loading transcript…

} {bodyStatus === 'notfound' && (

This transcript isn't published yet.

)} {bodyStatus === 'error' && (

Couldn't reach the session-history repo.

)} {bodyStatus === 'ok' && ( <>
)} )}
) }