Spaces:
Runtime error
Runtime error
File size: 7,846 Bytes
0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b 66de9c8 0649f1b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | import { useCallback, useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { artifactsApi, notebooksApi, reportsApi } from "../api/endpoints";
import type { Artifact, Citation, ReportSuggestion } from "../api/types";
import { ArtifactFeedback } from "../components/ArtifactFeedback";
import { ReportAddDialog } from "../components/ReportAddDialog";
import { ShareDialog } from "../components/ShareDialog";
import { Reader } from "../components/Reader";
import { reportMarkdown, ReportView } from "../components/ReportView";
import { ConfirmButton, errorMessage, JobProgress, Loading } from "../components/ui";
import { useJob } from "../hooks/useJob";
/** One report, full page: /app/reports/:id. */
export default function ReportPage() {
const { id = "" } = useParams();
const navigate = useNavigate();
const [artifact, setArtifact] = useState<Artifact | null>(null);
const [error, setError] = useState<string | null>(null);
const [adding, setAdding] = useState<{ suggestion: ReportSuggestion; existing: Artifact[]; loading: boolean } | null>(null);
const [sharing, setSharing] = useState(false);
const [addFailed, setAddFailed] = useState<string | null>(null);
const [reading, setReading] = useState<{ id: string; start: number; end: number } | null>(null);
const [removingId, setRemovingId] = useState<string | null>(null);
const load = useCallback(() => artifactsApi.get(id).then(setArtifact, () => setError("Report not found.")), [id]);
useEffect(() => {
void load();
}, [load]);
const [addingId, setAddingId] = useState<string | null>(null); // the suggestion being built (its card shows a loader)
const job = useJob(artifact?.job, () => {
void load();
setAddingId(null);
});
// If the live updates drop, the loader still ends once the job's own state is final. Only the job that adds a visual counts: the
// report's own, long finished job is what the page still holds in the moment after Add is clicked.
useEffect(() => {
if (addingId && job && job.kind === "report_block" && job.id === artifact?.job?.id && (job.status === "done" || job.status === "failed")) {
setAddingId(null);
void load();
}
}, [job, addingId, artifact?.job?.id, load]);
// An infographic added to the report is drawn after the report page shows it: look again until it is ready.
const drawing = (artifact?.content.blocks ?? []).some((b: { type: string; status?: string }) =>
b.type === "infographic" && ["pending", "generating", "rendering"].includes(b.status ?? ""));
useEffect(() => {
if (!drawing) return;
const t = setTimeout(() => void load(), 2500);
return () => clearTimeout(t);
}, [drawing, artifact, load]);
if (error) return <p className="error-text">{error}</p>;
if (!artifact) return <Loading label="Opening report" />;
const running = job && (job.status === "queued" || job.status === "running");
const adding_ = !!addingId || !!(running && job?.kind === "report_block"); // a visual is being added: one at a time
// Which suggestion's card shows the loader: the one just clicked, or, after a reload or a dropped update, the one the running job is adding.
const buildingId = addingId ?? (running && job?.kind === "report_block" ? (job.params.suggestion_id as string | undefined) ?? null : null);
const blockFailed = job?.kind === "report_block" && job.status === "failed" ? job.error : null;
const back = artifact.notebook_id ? `/app/notebooks/${artifact.notebook_id}` : "/app/archive";
const download = () => {
const url = URL.createObjectURL(new Blob([reportMarkdown(artifact.content)], { type: "text/markdown" }));
const a = document.createElement("a");
a.href = url;
a.download = `${artifact.title.slice(0, 60)}.md`;
a.click();
URL.revokeObjectURL(url);
};
const startAdd = async (s: ReportSuggestion) => {
setAddFailed(null);
setAdding({ suggestion: s, existing: [], loading: true });
// The ones already made from this notebook (or, for a report with no notebook, anywhere), to offer as an alternative
// to making a new one.
const all = artifact.notebook_id
? await notebooksApi.artifacts(artifact.notebook_id).catch(() => [] as Artifact[])
: await artifactsApi.list({ type: s.kind, status: "ready", limit: 50 }).then((p) => p.items).catch(() => [] as Artifact[]);
setAdding((cur) => cur && cur.suggestion.id === s.id
? { ...cur, loading: false, existing: all.filter((a) => a.type === s.kind && a.status === "ready") } : cur);
};
// Add closes the dialog at once and the suggestion's own card shows the loader, down in the report where the visual will appear.
const add = async (existingId: string | null, theme?: string) => {
if (!adding) return;
const { suggestion } = adding;
setAddFailed(null);
setAddingId(suggestion.id);
setAdding(null);
try {
setArtifact(await reportsApi.addBlock(artifact.id, {
kind: suggestion.kind, after_block_id: suggestion.after_block_id, suggestion_id: suggestion.id,
brief: suggestion.brief, existing_artifact_id: existingId ?? undefined, theme,
}));
} catch (e) {
setAddingId(null);
setAddFailed(errorMessage(e));
}
};
const remove = async (blockId: string) => {
setRemovingId(blockId);
try {
setArtifact(await reportsApi.removeBlock(artifact.id, blockId));
} catch (e) {
setError(errorMessage(e));
} finally {
setRemovingId(null);
}
};
const cite = (c: Citation) => c.document_id && setReading({ id: c.document_id, start: c.line_start, end: c.line_end });
return (
<div className="page-wrap rp-page">
<Link to={back} className="vw-back">Back</Link>
<header className="rp-head">
<p className="eyebrow">{artifact.content.format === "interactive" ? "Interactive report" : "Report"}</p>
<h1>{artifact.title}</h1>
{artifact.status === "ready" && (
<div className="row">
<button type="button" className="btn btn-small btn-primary" onClick={() => setSharing(true)}>Share</button>
<button type="button" className="btn btn-small" onClick={download}>Download Markdown</button>
<ArtifactFeedback artifact={artifact} label="How is this report?" compact />
<ConfirmButton onConfirm={async () => {
try {
await artifactsApi.remove(artifact.id);
navigate(back);
} catch (e) {
setError(errorMessage(e));
}
}}>Delete</ConfirmButton>
</div>
)}
</header>
{running && job && job.kind !== "report_block" && <JobProgress job={job} />}
{artifact.status === "failed" && <p className="error-text">{(artifact.content.error as string | undefined) ?? job?.error ?? "The report could not be made."}</p>}
{(addFailed ?? blockFailed) && <p className="error-text">{addFailed ?? blockFailed}</p>}
{artifact.status === "ready" && (
<ReportView content={artifact.content} artifactId={artifact.id} onCite={cite}
editor={{ onAdd: startAdd, onRemove: remove, busy: adding_, adding: buildingId,
addingMessage: job?.kind === "report_block" ? job.message : null, removing: removingId }} />
)}
{sharing && <ShareDialog artifactId={artifact.id} onClose={() => setSharing(false)} />}
{adding && <ReportAddDialog suggestion={adding.suggestion} existing={adding.existing} loading={adding.loading} busy={false} error={null}
onClose={() => setAdding(null)} onAdd={add} />}
{reading && <Reader documentId={reading.id} highlight={{ start: reading.start, end: reading.end }} onClose={() => setReading(null)} />}
</div>
);
}
|