pxengine 0.1.113 → 0.1.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -16125,6 +16125,22 @@ var ShareIcon = () => /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("svg", { w
16125
16125
  /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
16126
16126
  ] });
16127
16127
  var CheckIcon = () => /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("polyline", { points: "20 6 9 17 4 12" }) });
16128
+ var RefreshIcon = () => /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
16129
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("polyline", { points: "23 4 23 10 17 10" }),
16130
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
16131
+ ] });
16132
+ var SlideTypeIcon = () => /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
16133
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("rect", { x: "3", y: "4", width: "18", height: "14", rx: "2" }),
16134
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("line", { x1: "3", y1: "10", x2: "21", y2: "10" })
16135
+ ] });
16136
+ function formatTemplateLabel(templateId) {
16137
+ if (!templateId) return null;
16138
+ return templateId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
16139
+ }
16140
+ function deriveJobStatusUrl(newJobId, pollUrl, regenerateUrl) {
16141
+ const base = pollUrl?.replace(/\/api\/jobs\/[^/]+\/status.*$/, "") ?? regenerateUrl?.replace(/\/api\/presentations\/[^/]+\/regenerate.*$/, "") ?? "";
16142
+ return `${base}/api/jobs/${newJobId}/status`;
16143
+ }
16128
16144
  var FORMATS = [
16129
16145
  {
16130
16146
  key: "pptx_url",
@@ -16330,11 +16346,20 @@ var PresentationJobCard = ({
16330
16346
  status: initialStatus,
16331
16347
  slide_count: initialSlideCount,
16332
16348
  formats: initialFormats = {},
16349
+ generation_mode: initialGenerationMode,
16350
+ template_id: initialTemplateId,
16351
+ template_version_id: initialTemplateVersionId,
16352
+ review_status: initialReviewStatus,
16353
+ outline: initialOutline,
16333
16354
  error: initialError,
16334
16355
  progress: initialProgress,
16335
16356
  pollUrl,
16336
16357
  authToken,
16337
16358
  shareUrl,
16359
+ approveUrl,
16360
+ approveOutlineUrl,
16361
+ regenerateUrl,
16362
+ hideApproval = false,
16338
16363
  className,
16339
16364
  theme,
16340
16365
  onComplete,
@@ -16348,9 +16373,23 @@ var PresentationJobCard = ({
16348
16373
  const [formats, setFormats] = (0, import_react78.useState)(initialFormats);
16349
16374
  const [error, setError] = (0, import_react78.useState)(initialError);
16350
16375
  const [progress, setProgress] = (0, import_react78.useState)(initialProgress);
16376
+ const [generationMode, setGenerationMode] = (0, import_react78.useState)(
16377
+ initialGenerationMode || (initialFormats?.html_url ? "template" : "")
16378
+ );
16379
+ const [templateId, setTemplateId] = (0, import_react78.useState)(initialTemplateId || "");
16380
+ const [, setTemplateVersionId] = (0, import_react78.useState)(initialTemplateVersionId || "");
16381
+ const [reviewStatus, setReviewStatus] = (0, import_react78.useState)(initialReviewStatus || "");
16382
+ const [outline, setOutline] = (0, import_react78.useState)(initialOutline);
16351
16383
  const [showExport, setShowExport] = (0, import_react78.useState)(false);
16352
16384
  const [showFullscreen, setShowFullscreen] = (0, import_react78.useState)(false);
16353
16385
  const [copied, setCopied] = (0, import_react78.useState)(false);
16386
+ const [approving, setApproving] = (0, import_react78.useState)(false);
16387
+ const [regenerating, setRegenerating] = (0, import_react78.useState)(false);
16388
+ const [approveError, setApproveError] = (0, import_react78.useState)(null);
16389
+ const [messageEdits, setMessageEdits] = (0, import_react78.useState)({});
16390
+ const [approvingOutline, setApprovingOutline] = (0, import_react78.useState)(false);
16391
+ const [outlineWritePollUrl, setOutlineWritePollUrl] = (0, import_react78.useState)(null);
16392
+ const [regenPollUrl, setRegenPollUrl] = (0, import_react78.useState)(null);
16354
16393
  const [currentSlide, setCurrentSlide] = (0, import_react78.useState)(1);
16355
16394
  const [previewScale, setPreviewScale] = (0, import_react78.useState)(1);
16356
16395
  const [iframeReady, setIframeReady] = (0, import_react78.useState)(false);
@@ -16377,6 +16416,22 @@ var PresentationJobCard = ({
16377
16416
  (0, import_react78.useEffect)(() => {
16378
16417
  if (initialTitle) setTitle(initialTitle);
16379
16418
  }, [initialTitle]);
16419
+ (0, import_react78.useEffect)(() => {
16420
+ if (initialGenerationMode) setGenerationMode(initialGenerationMode);
16421
+ }, [initialGenerationMode]);
16422
+ (0, import_react78.useEffect)(() => {
16423
+ if (initialTemplateId) setTemplateId(initialTemplateId);
16424
+ }, [initialTemplateId]);
16425
+ (0, import_react78.useEffect)(() => {
16426
+ if (initialTemplateVersionId) setTemplateVersionId(initialTemplateVersionId);
16427
+ }, [initialTemplateVersionId]);
16428
+ (0, import_react78.useEffect)(() => {
16429
+ if (initialReviewStatus) setReviewStatus(initialReviewStatus);
16430
+ }, [initialReviewStatus]);
16431
+ const initialOutlineSlideCount = initialOutline?.slides?.length;
16432
+ (0, import_react78.useEffect)(() => {
16433
+ if (initialOutline) setOutline(initialOutline);
16434
+ }, [initialOutlineSlideCount]);
16380
16435
  const updateScale = (0, import_react78.useCallback)(() => {
16381
16436
  if (previewRef.current) setPreviewScale(previewRef.current.offsetWidth / 1280);
16382
16437
  }, []);
@@ -16440,13 +16495,21 @@ var PresentationJobCard = ({
16440
16495
  setProgress(data.progress);
16441
16496
  }
16442
16497
  if (newStatus === "complete" && data.output) {
16443
- const newTitle = data.output.title || initialTitle;
16444
- const newSlideCount = data.output.slide_count || 0;
16445
- const newFormats = data.output.formats || {};
16498
+ const output = data.output;
16499
+ const isOutlinePhase = output.review_status === "pending_outline_approval" || output.type === "presentation-outline";
16500
+ const newTitle = output.title || initialTitle;
16501
+ const newSlideCount = output.slide_count || 0;
16502
+ const newFormats = output.formats || {};
16446
16503
  setTitle(newTitle);
16447
16504
  setSlideCount(newSlideCount);
16448
16505
  setFormats(newFormats);
16449
- if (!hasNotifiedRef.current && onCompleteRef.current) {
16506
+ if (output.generation_mode) setGenerationMode(output.generation_mode);
16507
+ if (output.template_id) setTemplateId(output.template_id);
16508
+ if (output.template_version_id) setTemplateVersionId(output.template_version_id);
16509
+ if (output.review_status) setReviewStatus(output.review_status);
16510
+ if (output.outline) setOutline(output.outline);
16511
+ else if (output.deck_plan) setOutline(output.deck_plan);
16512
+ if (!isOutlinePhase && !hasNotifiedRef.current && onCompleteRef.current) {
16450
16513
  hasNotifiedRef.current = true;
16451
16514
  onCompleteRef.current({
16452
16515
  title: newTitle,
@@ -16466,6 +16529,184 @@ var PresentationJobCard = ({
16466
16529
  }
16467
16530
  // Transient fetch errors are ignored — the shared loop retries next tick.
16468
16531
  );
16532
+ const applyDeckOutput = (0, import_react78.useCallback)((out, opts) => {
16533
+ if (out.title) setTitle(out.title);
16534
+ if (out.slide_count !== void 0) setSlideCount(out.slide_count);
16535
+ if (out.formats) setFormats(out.formats);
16536
+ if (out.generation_mode) setGenerationMode(out.generation_mode);
16537
+ if (out.template_id) setTemplateId(out.template_id);
16538
+ setReviewStatus(out.review_status || "pending_review");
16539
+ setTemplateVersionId(opts?.regenerated ? "" : out.template_version_id || "");
16540
+ if (!opts?.regenerated && !hasNotifiedRef.current && onCompleteRef.current) {
16541
+ hasNotifiedRef.current = true;
16542
+ onCompleteRef.current({
16543
+ title: out.title ?? title,
16544
+ slide_count: out.slide_count ?? slideCount,
16545
+ formats: out.formats ?? formats
16546
+ });
16547
+ }
16548
+ }, [title, slideCount, formats]);
16549
+ const refetchSourceAndApply = (0, import_react78.useCallback)(async () => {
16550
+ if (!pollUrl) return;
16551
+ try {
16552
+ const headers = {};
16553
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
16554
+ const res = await fetch(pollUrl, { headers });
16555
+ if (!res.ok) return;
16556
+ const data = await res.json();
16557
+ if (data.output) applyDeckOutput(data.output);
16558
+ } catch {
16559
+ }
16560
+ }, [pollUrl, authToken, applyDeckOutput]);
16561
+ useSharedPoll(
16562
+ {
16563
+ key: outlineWritePollUrl,
16564
+ intervalMs: 3e3,
16565
+ fetcher: async () => {
16566
+ const headers = {};
16567
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
16568
+ const res = await fetch(outlineWritePollUrl, { headers });
16569
+ if (!res.ok) throw new Error(`poll ${res.status}`);
16570
+ return res.json();
16571
+ },
16572
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
16573
+ },
16574
+ (data) => {
16575
+ if (data.status === "complete") {
16576
+ setOutlineWritePollUrl(null);
16577
+ setApprovingOutline(false);
16578
+ void refetchSourceAndApply();
16579
+ } else if (data.status === "failed") {
16580
+ setApproveError(data.error || "Could not build the presentation");
16581
+ setOutlineWritePollUrl(null);
16582
+ setApprovingOutline(false);
16583
+ }
16584
+ }
16585
+ );
16586
+ useSharedPoll(
16587
+ {
16588
+ key: regenPollUrl,
16589
+ intervalMs: 3e3,
16590
+ fetcher: async () => {
16591
+ const headers = {};
16592
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
16593
+ const res = await fetch(regenPollUrl, { headers });
16594
+ if (!res.ok) throw new Error(`poll ${res.status}`);
16595
+ return res.json();
16596
+ },
16597
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
16598
+ },
16599
+ (data) => {
16600
+ if (data.status === "complete") {
16601
+ if (data.output) applyDeckOutput(data.output, { regenerated: true });
16602
+ setRegenPollUrl(null);
16603
+ setRegenerating(false);
16604
+ } else if (data.status === "failed") {
16605
+ setApproveError(data.error || "Could not switch template");
16606
+ setRegenPollUrl(null);
16607
+ setRegenerating(false);
16608
+ }
16609
+ }
16610
+ );
16611
+ const buildEditedOutline = () => {
16612
+ if (!outline || !Array.isArray(outline.slides)) return void 0;
16613
+ let changed = false;
16614
+ const slides = outline.slides.map((s, i) => {
16615
+ const edited = messageEdits[i];
16616
+ if (edited !== void 0 && edited !== s.message) {
16617
+ changed = true;
16618
+ return { ...s, message: edited };
16619
+ }
16620
+ return s;
16621
+ });
16622
+ return changed ? { ...outline, slides } : void 0;
16623
+ };
16624
+ const handleApproveOutline = async () => {
16625
+ if (!_job_id || approvingOutline) return;
16626
+ const endpoint = approveOutlineUrl || `/api/presentations/${_job_id}/approve-outline`;
16627
+ setApprovingOutline(true);
16628
+ setApproveError(null);
16629
+ try {
16630
+ const headers = { "Content-Type": "application/json" };
16631
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
16632
+ const editedOutline = buildEditedOutline();
16633
+ const res = await fetch(endpoint, {
16634
+ method: "POST",
16635
+ headers,
16636
+ body: JSON.stringify(editedOutline ? { outline: editedOutline } : {})
16637
+ });
16638
+ if (!res.ok) {
16639
+ const err = await res.json().catch(() => ({}));
16640
+ throw new Error(err.detail || err.error || `Approve failed (${res.status})`);
16641
+ }
16642
+ const data = await res.json();
16643
+ if (typeof data.job_id === "string" && data.job_id !== _job_id) {
16644
+ setOutlineWritePollUrl(deriveJobStatusUrl(data.job_id, pollUrl, regenerateUrl));
16645
+ return;
16646
+ }
16647
+ setApprovingOutline(false);
16648
+ void refetchSourceAndApply();
16649
+ } catch (e) {
16650
+ setApproveError(e instanceof Error ? e.message : "Could not build the presentation");
16651
+ setApprovingOutline(false);
16652
+ }
16653
+ };
16654
+ const handleApprove = async () => {
16655
+ if (!_job_id || approving) return;
16656
+ const endpoint = approveUrl || `/api/presentations/${_job_id}/approve-standard`;
16657
+ setApproving(true);
16658
+ setApproveError(null);
16659
+ try {
16660
+ const headers = { "Content-Type": "application/json" };
16661
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
16662
+ const res = await fetch(endpoint, {
16663
+ method: "POST",
16664
+ headers,
16665
+ body: JSON.stringify({ label: title })
16666
+ });
16667
+ if (!res.ok) {
16668
+ const err = await res.json().catch(() => ({}));
16669
+ throw new Error(err.detail || err.error || `Approve failed (${res.status})`);
16670
+ }
16671
+ const data = await res.json();
16672
+ setReviewStatus(data.review_status || "approved");
16673
+ if (data.template_version_id) setTemplateVersionId(data.template_version_id);
16674
+ if (data.template_id) setTemplateId(data.template_id);
16675
+ } catch (e) {
16676
+ setApproveError(e instanceof Error ? e.message : "Could not save the format");
16677
+ } finally {
16678
+ setApproving(false);
16679
+ }
16680
+ };
16681
+ const handleRegenerate = async () => {
16682
+ if (!_job_id || regenerating) return;
16683
+ const endpoint = regenerateUrl || `/api/presentations/${_job_id}/regenerate`;
16684
+ setRegenerating(true);
16685
+ setApproveError(null);
16686
+ try {
16687
+ const headers = { "Content-Type": "application/json" };
16688
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
16689
+ const res = await fetch(endpoint, {
16690
+ method: "POST",
16691
+ headers,
16692
+ body: JSON.stringify({})
16693
+ });
16694
+ if (!res.ok) {
16695
+ const err = await res.json().catch(() => ({}));
16696
+ throw new Error(err.detail || err.error || `Regenerate failed (${res.status})`);
16697
+ }
16698
+ const data = await res.json();
16699
+ if (typeof data.job_id === "string" && data.job_id !== _job_id) {
16700
+ setRegenPollUrl(deriveJobStatusUrl(data.job_id, pollUrl, regenerateUrl));
16701
+ return;
16702
+ }
16703
+ applyDeckOutput(data.output || data, { regenerated: true });
16704
+ setRegenerating(false);
16705
+ } catch (e) {
16706
+ setApproveError(e instanceof Error ? e.message : "Could not switch template");
16707
+ setRegenerating(false);
16708
+ }
16709
+ };
16469
16710
  const handleShare = async () => {
16470
16711
  const link = shareUrl ?? (typeof window !== "undefined" && _job_id ? `${window.location.origin}/p/${_job_id}` : formats.html_url);
16471
16712
  if (!link) return;
@@ -16586,8 +16827,85 @@ var PresentationJobCard = ({
16586
16827
  ] })
16587
16828
  ] }) }) });
16588
16829
  }
16830
+ if (status === "complete" && reviewStatus === "pending_outline_approval") {
16831
+ const slides = Array.isArray(outline?.slides) ? outline.slides : [];
16832
+ const building = Boolean(outlineWritePollUrl) || approvingOutline;
16833
+ return /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("div", { className: cn("w-full", className), children: /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", style: t.root, children: [
16834
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(
16835
+ "div",
16836
+ {
16837
+ className: "h-[3px]",
16838
+ style: { background: accentGradient || "linear-gradient(90deg, #8B7D5E, #C0AE82, #DCC99B)" }
16839
+ }
16840
+ ),
16841
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "p-5", children: [
16842
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "flex items-start gap-3", children: [
16843
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(
16844
+ "div",
16845
+ {
16846
+ className: "w-10 h-10 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center flex-shrink-0 text-indigo-400",
16847
+ style: { ...t.accentSubtle(0.1), ...t.accentBorder, ...t.accent },
16848
+ children: /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(SlidesIcon, {})
16849
+ }
16850
+ ),
16851
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "flex-1 min-w-0", children: [
16852
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("h3", { className: "text-sm font-semibold text-zinc-100 truncate leading-tight", style: t.text, children: title || "Presentation outline" }),
16853
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("p", { className: "text-xs text-amber-400/90 mt-0.5", children: [
16854
+ "Review the slide outline",
16855
+ slides.length > 0 ? " \u2014 edit any slide message" : "",
16856
+ ", then build the deck."
16857
+ ] })
16858
+ ] })
16859
+ ] }),
16860
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "mt-4 space-y-2", children: [
16861
+ slides.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "flex items-center gap-2", children: [
16862
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { className: "text-xs text-zinc-600 w-5 text-right tabular-nums flex-shrink-0", children: s.n ?? i + 1 }),
16863
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)(
16864
+ "span",
16865
+ {
16866
+ className: "flex items-center gap-1 text-[10px] uppercase tracking-wide text-zinc-500 w-20 flex-shrink-0 truncate",
16867
+ title: s.type,
16868
+ children: [
16869
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(SlideTypeIcon, {}),
16870
+ s.type || "slide"
16871
+ ]
16872
+ }
16873
+ ),
16874
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(
16875
+ "input",
16876
+ {
16877
+ value: messageEdits[i] ?? s.message ?? "",
16878
+ onChange: (e) => setMessageEdits((prev) => ({ ...prev, [i]: e.target.value })),
16879
+ disabled: building,
16880
+ className: "flex-1 min-w-0 bg-zinc-800/60 border border-zinc-700 rounded-lg px-3 h-8 text-xs text-zinc-200 focus:border-indigo-500 outline-none disabled:opacity-60"
16881
+ }
16882
+ )
16883
+ ] }, i)),
16884
+ slides.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("p", { className: "text-xs text-zinc-500", children: "No outline slides were returned; you can still build the deck." })
16885
+ ] }),
16886
+ approveError && /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("p", { className: "mt-3 text-xs text-red-400/90", children: approveError }),
16887
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("div", { className: "mt-4 flex items-center justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)(
16888
+ "button",
16889
+ {
16890
+ onClick: handleApproveOutline,
16891
+ disabled: building,
16892
+ title: "Approve this outline and build the full deck",
16893
+ className: "flex items-center gap-1.5 px-3 h-8 rounded-lg border border-emerald-700/50 bg-emerald-950/40 hover:border-emerald-500 text-emerald-300 text-xs font-medium transition-colors disabled:opacity-50",
16894
+ children: [
16895
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(CheckIcon, {}),
16896
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { children: building ? "Building deck..." : "Approve & Build" })
16897
+ ]
16898
+ }
16899
+ ) })
16900
+ ] })
16901
+ ] }) });
16902
+ }
16589
16903
  const total = slideCount || 1;
16590
16904
  const hasHtml = Boolean(formats.html_url);
16905
+ const templateLabel = formatTemplateLabel(templateId);
16906
+ const isApproved = reviewStatus === "approved";
16907
+ const canApprove = !hideApproval && generationMode === "template" && reviewStatus === "pending_review" && Boolean(_job_id);
16908
+ const canRegenerate = canApprove;
16591
16909
  return /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)(import_jsx_runtime147.Fragment, { children: [
16592
16910
  /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("div", { className: cn("w-full", className), children: /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", style: t.root, children: [
16593
16911
  /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("div", { className: "h-[3px]", style: { background: accentGradient || "linear-gradient(90deg, #8B7D5E, #C0AE82, #DCC99B)" } }),
@@ -16605,10 +16923,39 @@ var PresentationJobCard = ({
16605
16923
  /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("h3", { className: "text-sm font-semibold text-zinc-100 truncate leading-tight", style: t.text, children: title }),
16606
16924
  /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("p", { className: "text-xs text-zinc-500 mt-0.5", style: t.muted, children: [
16607
16925
  slideCount ? `${slideCount} slides` : "Presentation",
16608
- " \xB7 Ready to export"
16609
- ] })
16926
+ " \xB7 Ready to export",
16927
+ templateLabel && ` \xB7 ${templateLabel}`
16928
+ ] }),
16929
+ generationMode === "template" && /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("p", { className: "text-xs mt-1", children: isApproved ? /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { className: "text-emerald-400/90", children: "Approved \u2014 future decks use this format" }) : reviewStatus === "pending_review" ? /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { className: "text-amber-400/90", children: "Review this deck, then approve to lock the format" }) : null })
16610
16930
  ] }),
16611
- /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [
16931
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "flex items-center gap-1.5 flex-shrink-0 flex-wrap justify-end", children: [
16932
+ canRegenerate && /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)(
16933
+ "button",
16934
+ {
16935
+ onClick: handleRegenerate,
16936
+ disabled: regenerating || approving,
16937
+ title: "Try another design template with the same content",
16938
+ className: "flex items-center gap-1.5 px-2.5 h-7 rounded-lg border border-zinc-700 bg-zinc-800 hover:border-zinc-500 text-zinc-300 text-xs font-medium transition-colors disabled:opacity-50",
16939
+ children: [
16940
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(RefreshIcon, {}),
16941
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { children: regenerating ? "Switching..." : "Regenerate" })
16942
+ ]
16943
+ }
16944
+ ),
16945
+ canApprove && /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)(
16946
+ "button",
16947
+ {
16948
+ onClick: handleApprove,
16949
+ disabled: approving,
16950
+ title: "Approve and save this deck format for future runs",
16951
+ className: "flex items-center gap-1.5 px-2.5 h-7 rounded-lg border border-emerald-700/50 bg-emerald-950/40 hover:border-emerald-500 text-emerald-300 text-xs font-medium transition-colors disabled:opacity-50",
16952
+ children: [
16953
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(CheckIcon, {}),
16954
+ /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { children: approving ? "Saving..." : "Approve" })
16955
+ ]
16956
+ }
16957
+ ),
16958
+ isApproved && /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("span", { className: "flex items-center justify-center w-7 h-7 rounded-lg border border-emerald-500/40 bg-emerald-500/10 text-emerald-400", title: "Format saved", children: /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(CheckIcon, {}) }),
16612
16959
  /* @__PURE__ */ (0, import_jsx_runtime147.jsx)(
16613
16960
  "button",
16614
16961
  {
@@ -16650,6 +16997,7 @@ var PresentationJobCard = ({
16650
16997
  )
16651
16998
  ] })
16652
16999
  ] }),
17000
+ approveError && /* @__PURE__ */ (0, import_jsx_runtime147.jsx)("p", { className: "mt-3 text-xs text-red-400/90", children: approveError }),
16653
17001
  hasHtml && /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)("div", { className: "mt-4", children: [
16654
17002
  /* @__PURE__ */ (0, import_jsx_runtime147.jsxs)(
16655
17003
  "div",
@@ -16784,11 +17132,11 @@ var ShareIcon2 = () => /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("svg", {
16784
17132
  /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
16785
17133
  ] });
16786
17134
  var CheckIcon2 = () => /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("polyline", { points: "20 6 9 17 4 12" }) });
16787
- var RefreshIcon = () => /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
17135
+ var RefreshIcon2 = () => /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
16788
17136
  /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("polyline", { points: "23 4 23 10 17 10" }),
16789
17137
  /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
16790
17138
  ] });
16791
- function formatTemplateLabel(templateId) {
17139
+ function formatTemplateLabel2(templateId) {
16792
17140
  if (!templateId) return null;
16793
17141
  return templateId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
16794
17142
  }
@@ -16835,7 +17183,7 @@ var FullscreenPreviewModal = ({ url, title, onClose }) => {
16835
17183
  ) })
16836
17184
  ] });
16837
17185
  };
16838
- function deriveJobStatusUrl(newJobId, pollUrl, regenerateUrl) {
17186
+ function deriveJobStatusUrl2(newJobId, pollUrl, regenerateUrl) {
16839
17187
  const base = pollUrl?.replace(/\/api\/jobs\/[^/]+\/status.*$/, "") ?? regenerateUrl?.replace(/\/api\/reports\/[^/]+\/regenerate.*$/, "") ?? "";
16840
17188
  return `${base}/api/jobs/${newJobId}/status`;
16841
17189
  }
@@ -16856,6 +17204,7 @@ var ResearchReportJobCard = (props) => {
16856
17204
  template_id: initialTemplateId,
16857
17205
  template_version_id: initialTemplateVersionId,
16858
17206
  review_status: initialReviewStatus,
17207
+ outline: initialOutline,
16859
17208
  theme: initialTheme,
16860
17209
  error: initialError,
16861
17210
  progress: initialProgress,
@@ -16863,7 +17212,9 @@ var ResearchReportJobCard = (props) => {
16863
17212
  authToken,
16864
17213
  shareUrl,
16865
17214
  approveUrl,
17215
+ approveOutlineUrl,
16866
17216
  regenerateUrl,
17217
+ hideApproval = false,
16867
17218
  className,
16868
17219
  onComplete,
16869
17220
  onFailed,
@@ -16886,6 +17237,7 @@ var ResearchReportJobCard = (props) => {
16886
17237
  const [reviewStatus, setReviewStatus] = (0, import_react79.useState)(
16887
17238
  initialReviewStatus || (initialHtmlUrl ? "pending_review" : "")
16888
17239
  );
17240
+ const [outline, setOutline] = (0, import_react79.useState)(initialOutline);
16889
17241
  const [theme, setTheme] = (0, import_react79.useState)(initialTheme || DEFAULT_THEME);
16890
17242
  const [error, setError] = (0, import_react79.useState)(initialError);
16891
17243
  const [progress, setProgress] = (0, import_react79.useState)(initialProgress);
@@ -16895,6 +17247,9 @@ var ResearchReportJobCard = (props) => {
16895
17247
  const [approving, setApproving] = (0, import_react79.useState)(false);
16896
17248
  const [regenerating, setRegenerating] = (0, import_react79.useState)(false);
16897
17249
  const [approveError, setApproveError] = (0, import_react79.useState)(null);
17250
+ const [headingEdits, setHeadingEdits] = (0, import_react79.useState)({});
17251
+ const [approvingOutline, setApprovingOutline] = (0, import_react79.useState)(false);
17252
+ const [outlineWritePollUrl, setOutlineWritePollUrl] = (0, import_react79.useState)(null);
16898
17253
  const [regenPollUrl, setRegenPollUrl] = (0, import_react79.useState)(null);
16899
17254
  const previewRef = (0, import_react79.useRef)(null);
16900
17255
  const onCompleteRef = (0, import_react79.useRef)(onComplete);
@@ -16924,6 +17279,10 @@ var ResearchReportJobCard = (props) => {
16924
17279
  (0, import_react79.useEffect)(() => {
16925
17280
  if (initialReviewStatus) setReviewStatus(initialReviewStatus);
16926
17281
  }, [initialReviewStatus]);
17282
+ const initialOutlineSectionCount = initialOutline?.sections?.length;
17283
+ (0, import_react79.useEffect)(() => {
17284
+ if (initialOutline) setOutline(initialOutline);
17285
+ }, [initialOutlineSectionCount]);
16927
17286
  (0, import_react79.useEffect)(() => {
16928
17287
  if (initialDepth) setDepth(initialDepth);
16929
17288
  }, [initialDepth]);
@@ -16989,6 +17348,7 @@ var ResearchReportJobCard = (props) => {
16989
17348
  }
16990
17349
  if (newStatus === "complete" && data.output) {
16991
17350
  const output = data.output;
17351
+ const isOutlinePhase = output.review_status === "pending_outline_approval" || output.type === "research-report-outline";
16992
17352
  setTitle(output.title || initialTitle);
16993
17353
  setDepth(output.depth || "");
16994
17354
  setSectionCount(output.section_count || 0);
@@ -17000,10 +17360,11 @@ var ResearchReportJobCard = (props) => {
17000
17360
  if (output.template_id) setTemplateId(output.template_id);
17001
17361
  if (output.template_version_id) setTemplateVersionId(output.template_version_id);
17002
17362
  if (output.review_status) setReviewStatus(output.review_status);
17363
+ if (output.outline) setOutline(output.outline);
17003
17364
  if (output.theme) {
17004
17365
  setTheme(output.theme);
17005
17366
  }
17006
- if (!hasNotifiedRef.current && onCompleteRef.current) {
17367
+ if (!isOutlinePhase && !hasNotifiedRef.current && onCompleteRef.current) {
17007
17368
  hasNotifiedRef.current = true;
17008
17369
  onCompleteRef.current(output);
17009
17370
  }
@@ -17072,6 +17433,103 @@ var ResearchReportJobCard = (props) => {
17072
17433
  }
17073
17434
  }
17074
17435
  );
17436
+ const refetchSourceAndApply = (0, import_react79.useCallback)(async () => {
17437
+ if (!pollUrl) return;
17438
+ try {
17439
+ const headers = {};
17440
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
17441
+ const res = await fetch(pollUrl, { headers });
17442
+ if (!res.ok) return;
17443
+ const data = await res.json();
17444
+ const out = data.output;
17445
+ if (!out) return;
17446
+ if (out.title) setTitle(out.title);
17447
+ if (out.depth) setDepth(out.depth);
17448
+ if (out.section_count !== void 0) setSectionCount(out.section_count);
17449
+ if (out.source_count !== void 0) setSourceCount(out.source_count);
17450
+ if (out.word_count !== void 0) setWordCount(out.word_count);
17451
+ if (out.executive_summary) setSummary(out.executive_summary);
17452
+ if (out.html_url) setHtmlUrl(out.html_url);
17453
+ if (out.generation_mode) setGenerationMode(out.generation_mode);
17454
+ if (out.template_id) setTemplateId(out.template_id);
17455
+ setTemplateVersionId(out.template_version_id || "");
17456
+ setReviewStatus(out.review_status || "pending_review");
17457
+ if (out.theme) setTheme(out.theme);
17458
+ if (!hasNotifiedRef.current && onCompleteRef.current) {
17459
+ hasNotifiedRef.current = true;
17460
+ onCompleteRef.current(out);
17461
+ }
17462
+ } catch {
17463
+ }
17464
+ }, [pollUrl, authToken]);
17465
+ useSharedPoll(
17466
+ {
17467
+ key: outlineWritePollUrl,
17468
+ intervalMs: 3e3,
17469
+ fetcher: async () => {
17470
+ const headers = {};
17471
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
17472
+ const res = await fetch(outlineWritePollUrl, { headers });
17473
+ if (!res.ok) throw new Error(`poll ${res.status}`);
17474
+ return res.json();
17475
+ },
17476
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
17477
+ },
17478
+ (data) => {
17479
+ if (data.status === "complete") {
17480
+ setOutlineWritePollUrl(null);
17481
+ setApprovingOutline(false);
17482
+ void refetchSourceAndApply();
17483
+ } else if (data.status === "failed") {
17484
+ setApproveError(data.error || "Could not build the report");
17485
+ setOutlineWritePollUrl(null);
17486
+ setApprovingOutline(false);
17487
+ }
17488
+ }
17489
+ );
17490
+ const buildEditedOutline = () => {
17491
+ if (!outline || !Array.isArray(outline.sections)) return void 0;
17492
+ let changed = false;
17493
+ const sections = outline.sections.map((s, i) => {
17494
+ const edited = headingEdits[i];
17495
+ if (edited !== void 0 && edited !== s.heading) {
17496
+ changed = true;
17497
+ return { ...s, heading: edited };
17498
+ }
17499
+ return s;
17500
+ });
17501
+ return changed ? { ...outline, sections } : void 0;
17502
+ };
17503
+ const handleApproveOutline = async () => {
17504
+ if (!job_id || approvingOutline) return;
17505
+ const endpoint = approveOutlineUrl || `/api/reports/${job_id}/approve-outline`;
17506
+ setApprovingOutline(true);
17507
+ setApproveError(null);
17508
+ try {
17509
+ const headers = { "Content-Type": "application/json" };
17510
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
17511
+ const editedOutline = buildEditedOutline();
17512
+ const res = await fetch(endpoint, {
17513
+ method: "POST",
17514
+ headers,
17515
+ body: JSON.stringify(editedOutline ? { outline: editedOutline } : {})
17516
+ });
17517
+ if (!res.ok) {
17518
+ const err = await res.json().catch(() => ({}));
17519
+ throw new Error(err.detail || err.error || `Approve failed (${res.status})`);
17520
+ }
17521
+ const data = await res.json();
17522
+ if (typeof data.job_id === "string" && data.job_id !== job_id) {
17523
+ setOutlineWritePollUrl(deriveJobStatusUrl2(data.job_id, pollUrl, regenerateUrl));
17524
+ return;
17525
+ }
17526
+ setApprovingOutline(false);
17527
+ void refetchSourceAndApply();
17528
+ } catch (e) {
17529
+ setApproveError(e instanceof Error ? e.message : "Could not build the report");
17530
+ setApprovingOutline(false);
17531
+ }
17532
+ };
17075
17533
  const formatWordCount = (count) => {
17076
17534
  if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
17077
17535
  return count.toString();
@@ -17177,6 +17635,61 @@ var ResearchReportJobCard = (props) => {
17177
17635
  ] })
17178
17636
  ] }) }) });
17179
17637
  }
17638
+ if (status === "complete" && reviewStatus === "pending_outline_approval") {
17639
+ const sections = Array.isArray(outline?.sections) ? outline.sections : [];
17640
+ const building = Boolean(outlineWritePollUrl) || approvingOutline;
17641
+ return /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("div", { className: cn("w-full", className), children: /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", children: [
17642
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(
17643
+ "div",
17644
+ {
17645
+ className: "h-[3px]",
17646
+ style: { background: "linear-gradient(90deg, #8B7D5E, #C0AE82, #DCC99B)" }
17647
+ }
17648
+ ),
17649
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "p-5", children: [
17650
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "flex items-start gap-3", children: [
17651
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("div", { className: "w-10 h-10 rounded-xl bg-violet-500/10 border border-violet-500/20 flex items-center justify-center flex-shrink-0 text-violet-300", children: /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(DocumentIcon, {}) }),
17652
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "flex-1 min-w-0", children: [
17653
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("h3", { className: "text-sm font-semibold text-zinc-100 truncate leading-tight", children: title || "Report outline" }),
17654
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("p", { className: "text-xs text-amber-400/90 mt-0.5", children: [
17655
+ "Review the outline",
17656
+ sections.length > 0 ? " \u2014 edit any heading" : "",
17657
+ ", then build the full report."
17658
+ ] })
17659
+ ] })
17660
+ ] }),
17661
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "mt-4 space-y-2", children: [
17662
+ sections.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "flex items-center gap-2", children: [
17663
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("span", { className: "text-xs text-zinc-600 w-5 text-right tabular-nums flex-shrink-0", children: i + 1 }),
17664
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(
17665
+ "input",
17666
+ {
17667
+ value: headingEdits[i] ?? s.heading ?? "",
17668
+ onChange: (e) => setHeadingEdits((prev) => ({ ...prev, [i]: e.target.value })),
17669
+ disabled: building,
17670
+ className: "flex-1 min-w-0 bg-zinc-800/60 border border-zinc-700 rounded-lg px-3 h-8 text-xs text-zinc-200 focus:border-violet-500 outline-none disabled:opacity-60"
17671
+ }
17672
+ )
17673
+ ] }, i)),
17674
+ sections.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("p", { className: "text-xs text-zinc-500", children: "No outline sections were returned; you can still build the report." })
17675
+ ] }),
17676
+ approveError && /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("p", { className: "mt-3 text-xs text-red-400/90", children: approveError }),
17677
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("div", { className: "mt-4 flex items-center justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)(
17678
+ "button",
17679
+ {
17680
+ onClick: handleApproveOutline,
17681
+ disabled: building,
17682
+ title: "Approve this outline and build the full report",
17683
+ className: "flex items-center gap-1.5 px-3 h-8 rounded-lg border border-emerald-700/50 bg-emerald-950/40 hover:border-emerald-500 text-emerald-300 text-xs font-medium transition-colors disabled:opacity-50",
17684
+ children: [
17685
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(CheckIcon2, {}),
17686
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("span", { children: building ? "Building report..." : "Approve & Build" })
17687
+ ]
17688
+ }
17689
+ ) })
17690
+ ] })
17691
+ ] }) });
17692
+ }
17180
17693
  if (status === "complete" && !hasHTML) {
17181
17694
  return /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("div", { className: cn("w-full", className), children: /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", children: [
17182
17695
  /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(
@@ -17240,8 +17753,8 @@ var ResearchReportJobCard = (props) => {
17240
17753
  window.open(link, "_blank", "noopener,noreferrer");
17241
17754
  }
17242
17755
  };
17243
- const canApprove = generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
17244
- const canRegenerate = generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
17756
+ const canApprove = !hideApproval && generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
17757
+ const canRegenerate = !hideApproval && generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
17245
17758
  const canShare = Boolean(hasHTML || job_id) && (generationMode !== "template" || reviewStatus !== "pending_review");
17246
17759
  const handleApprove = async () => {
17247
17760
  if (!job_id || approving) return;
@@ -17306,7 +17819,7 @@ var ResearchReportJobCard = (props) => {
17306
17819
  }
17307
17820
  const data = await res.json();
17308
17821
  if (typeof data.job_id === "string" && data.job_id !== job_id) {
17309
- setRegenPollUrl(deriveJobStatusUrl(data.job_id, pollUrl, regenerateUrl));
17822
+ setRegenPollUrl(deriveJobStatusUrl2(data.job_id, pollUrl, regenerateUrl));
17310
17823
  return;
17311
17824
  }
17312
17825
  applyRegeneratedOutput(data.output || data);
@@ -17316,7 +17829,7 @@ var ResearchReportJobCard = (props) => {
17316
17829
  setRegenerating(false);
17317
17830
  }
17318
17831
  };
17319
- const templateLabel = formatTemplateLabel(templateId);
17832
+ const templateLabel = formatTemplateLabel2(templateId);
17320
17833
  const isApproved = reviewStatus === "approved";
17321
17834
  return /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)(import_jsx_runtime148.Fragment, { children: [
17322
17835
  /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("div", { className: cn("w-full", className), children: /* @__PURE__ */ (0, import_jsx_runtime148.jsxs)("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", children: [
@@ -17365,7 +17878,7 @@ var ResearchReportJobCard = (props) => {
17365
17878
  title: "Try another report template with the same data",
17366
17879
  className: "flex items-center gap-1.5 px-2.5 h-7 rounded-lg border border-zinc-700 bg-zinc-800 hover:border-zinc-500 text-zinc-300 text-xs font-medium transition-colors disabled:opacity-50",
17367
17880
  children: [
17368
- /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(RefreshIcon, {}),
17881
+ /* @__PURE__ */ (0, import_jsx_runtime148.jsx)(RefreshIcon2, {}),
17369
17882
  /* @__PURE__ */ (0, import_jsx_runtime148.jsx)("span", { children: regenerating ? "Switching..." : "Regenerate" })
17370
17883
  ]
17371
17884
  }