import path from "node:path"; import Link from "next/link"; import { notFound } from "next/navigation"; import { readRevisionHead, REPORT_HISTORY_GIT_DIRNAME } from "yt-dlp-transcript-common/publish/reportHistory"; import { siteReportDir } from "yt-dlp-transcript-common/publish/reportMedia"; import { hasSlides, slideLint } from "yt-dlp-transcript-common/lib/report/slides"; import BrowseHeader from "@/components/BrowseHeader"; import ArticleReader from "@/components/articles/ArticleReader"; import WorkspacePanel from "@/components/articles/WorkspacePanel"; import { badgeVariants } from "@/components/ui/badge"; import { articleView } from "@/lib/articles/article"; import { articleWorkspaceListing, openWorkspaceFile } from "@/lib/articles/files"; import { linkedProjects } from "@/lib/articles/links.mjs"; import { readArticleNotes, readReportFile, siteById, sitesPaths } from "@/lib/articles/sites"; import { sourceFor } from "@/lib/articles/sources.mjs"; import { corpusNotesFile } from "@/lib/paths"; import type { NotesRead } from "@/lib/annotations/types"; export const dynamic = "force-dynamic"; // One article: the reader with its notes (default), or `?tab=source` -- the // workspace files it was written from, its draft marked, and what would make // its slides read badly (lib/report/slides.ts slideLint, and the validator's // slide problems). // // ?rv=slides|iso the article as slides, or the overview (the // reader's own switch; the place is the hash) // ?status=open|resolved|all the notes filter it opens with // ?note= the note it opens on (the decisions inbox links here) // ?ws=&rel= a workspace file open on the source tab type Search = { tab?: string; status?: string; note?: string; ws?: string; rel?: string }; export default async function ArticlePage({ params, searchParams, }: { params: Promise<{ site: string; report: string }>; searchParams: Promise; }) { const { site: siteId, report: reportId } = await params; const sp = await searchParams; const site = siteById(siteId); if (!site) notFound(); const read = await readReportFile(site.siteId, reportId); if (!read.report && read.problems[0]?.message === "no report.json") notFound(); const [notes, source] = await Promise.all([readArticleNotes(site.siteId, reportId), sourceFor(site.siteId, reportId)]); const links = await linkedProjects(site.siteId, reportId, { workspace: source?.workspace ?? null }); const gitDir = path.join(/* turbopackIgnore: true */ siteReportDir(sitesPaths(), site.siteId, reportId), REPORT_HISTORY_GIT_DIRNAME); const head = await readRevisionHead(gitDir).catch(() => null); const published = (site.reports ?? []).includes(reportId); const r = read.report; const tab = sp.tab === "source" ? "source" : "article"; const base = `/sites/${site.siteId}/${reportId}`; const media = (file: string) => `/api/sites/media?site=${encodeURIComponent(site.siteId)}&report=${encodeURIComponent(reportId)}&file=${encodeURIComponent(file)}`; const meta = (
{site.siteTitle || site.siteId} {published ? "published" : "draft"} {r?.published && published {r.published}} {r?.updated && updated {r.updated}} {head && {head.revision} revision{head.revision === 1 ? "" : "s"}} {links.linked.map((p: { id: string }) => ( {p.id} ))} {links.possible.map((p: { id: string }) => ( {p.id} (possible) ))} {source && (
{source.draft && draft {source.draft}} {source.generator && generator {source.generator}}
)}
); const header = ( n.status === "open").length ?? 0} open notes`} /> ); if (!r) { return (
{header}
{meta}

report.json does not read as a report:

    {read.problems.slice(0, 20).map((p, i) => (
  • {p.message}
  • ))}
); } if (tab === "source") { const ws = await articleWorkspaceListing(source?.workspace); const opened = sp.ws && sp.rel ? await openWorkspaceFile(sp.ws, sp.rel) : ws && source?.draft ? await openWorkspaceFile(ws.name, path.relative(ws.dir, source.draft)) : null; const draftRel = ws && source?.draft ? `${ws.name}/${path.relative(ws.dir, source.draft)}` : null; const { view: slideView } = await articleView(r); const lint = hasSlides(slideView) ? slideLint(slideView) : []; const slideProblems = read.problems.filter((p) => /(^|\.)slides?(\.|\[|$)/.test(p.path ?? "")); return (
{header}
{meta} {hasSlides(slideView) && (
slides
{lint.length + slideProblems.length === 0 ? (

nothing to fix

) : (
    {slideProblems.map((p, i) => (
  • {p.path} {p.message}
  • ))} {lint.map((l, i) => (
  • {l.key} {" "} {l.message}
  • ))}
)}
)} `${base}?${new URLSearchParams({ tab: "source", ws: w, rel })}`} />
); } const { view, error } = await articleView(r); const initialNotes: NotesRead = { subject: { kind: "article", site: site.siteId, report: reportId }, file: corpusNotesFile(site.siteId, reportId) ?? "", token: notes.token, doc: notes.doc, source: notes.doc?.source ?? source ?? null, ...(notes.error ? { error: notes.error } : {}), }; const status = sp.status === "resolved" || sp.status === "all" ? sp.status : "open"; const video = r.video ? { file: r.video.src, src: media(r.video.src), poster: r.video.poster ? media(r.video.poster) : undefined, caption: r.video.caption } : null; return (
{header} ({ id: p.id, title: p.title, generatedBy: p.generatedBy }))} />
); }