pxengine 0.1.112 → 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.mjs CHANGED
@@ -15793,6 +15793,22 @@ var ShareIcon = () => /* @__PURE__ */ jsxs108("svg", { width: "14", height: "14"
15793
15793
  /* @__PURE__ */ jsx147("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
15794
15794
  ] });
15795
15795
  var CheckIcon = () => /* @__PURE__ */ jsx147("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx147("polyline", { points: "20 6 9 17 4 12" }) });
15796
+ var RefreshIcon = () => /* @__PURE__ */ jsxs108("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
15797
+ /* @__PURE__ */ jsx147("polyline", { points: "23 4 23 10 17 10" }),
15798
+ /* @__PURE__ */ jsx147("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
15799
+ ] });
15800
+ var SlideTypeIcon = () => /* @__PURE__ */ jsxs108("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
15801
+ /* @__PURE__ */ jsx147("rect", { x: "3", y: "4", width: "18", height: "14", rx: "2" }),
15802
+ /* @__PURE__ */ jsx147("line", { x1: "3", y1: "10", x2: "21", y2: "10" })
15803
+ ] });
15804
+ function formatTemplateLabel(templateId) {
15805
+ if (!templateId) return null;
15806
+ return templateId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
15807
+ }
15808
+ function deriveJobStatusUrl(newJobId, pollUrl, regenerateUrl) {
15809
+ const base = pollUrl?.replace(/\/api\/jobs\/[^/]+\/status.*$/, "") ?? regenerateUrl?.replace(/\/api\/presentations\/[^/]+\/regenerate.*$/, "") ?? "";
15810
+ return `${base}/api/jobs/${newJobId}/status`;
15811
+ }
15796
15812
  var FORMATS = [
15797
15813
  {
15798
15814
  key: "pptx_url",
@@ -15998,11 +16014,20 @@ var PresentationJobCard = ({
15998
16014
  status: initialStatus,
15999
16015
  slide_count: initialSlideCount,
16000
16016
  formats: initialFormats = {},
16017
+ generation_mode: initialGenerationMode,
16018
+ template_id: initialTemplateId,
16019
+ template_version_id: initialTemplateVersionId,
16020
+ review_status: initialReviewStatus,
16021
+ outline: initialOutline,
16001
16022
  error: initialError,
16002
16023
  progress: initialProgress,
16003
16024
  pollUrl,
16004
16025
  authToken,
16005
16026
  shareUrl,
16027
+ approveUrl,
16028
+ approveOutlineUrl,
16029
+ regenerateUrl,
16030
+ hideApproval = false,
16006
16031
  className,
16007
16032
  theme,
16008
16033
  onComplete,
@@ -16016,9 +16041,23 @@ var PresentationJobCard = ({
16016
16041
  const [formats, setFormats] = useState10(initialFormats);
16017
16042
  const [error, setError] = useState10(initialError);
16018
16043
  const [progress, setProgress] = useState10(initialProgress);
16044
+ const [generationMode, setGenerationMode] = useState10(
16045
+ initialGenerationMode || (initialFormats?.html_url ? "template" : "")
16046
+ );
16047
+ const [templateId, setTemplateId] = useState10(initialTemplateId || "");
16048
+ const [, setTemplateVersionId] = useState10(initialTemplateVersionId || "");
16049
+ const [reviewStatus, setReviewStatus] = useState10(initialReviewStatus || "");
16050
+ const [outline, setOutline] = useState10(initialOutline);
16019
16051
  const [showExport, setShowExport] = useState10(false);
16020
16052
  const [showFullscreen, setShowFullscreen] = useState10(false);
16021
16053
  const [copied, setCopied] = useState10(false);
16054
+ const [approving, setApproving] = useState10(false);
16055
+ const [regenerating, setRegenerating] = useState10(false);
16056
+ const [approveError, setApproveError] = useState10(null);
16057
+ const [messageEdits, setMessageEdits] = useState10({});
16058
+ const [approvingOutline, setApprovingOutline] = useState10(false);
16059
+ const [outlineWritePollUrl, setOutlineWritePollUrl] = useState10(null);
16060
+ const [regenPollUrl, setRegenPollUrl] = useState10(null);
16022
16061
  const [currentSlide, setCurrentSlide] = useState10(1);
16023
16062
  const [previewScale, setPreviewScale] = useState10(1);
16024
16063
  const [iframeReady, setIframeReady] = useState10(false);
@@ -16045,6 +16084,22 @@ var PresentationJobCard = ({
16045
16084
  useEffect7(() => {
16046
16085
  if (initialTitle) setTitle(initialTitle);
16047
16086
  }, [initialTitle]);
16087
+ useEffect7(() => {
16088
+ if (initialGenerationMode) setGenerationMode(initialGenerationMode);
16089
+ }, [initialGenerationMode]);
16090
+ useEffect7(() => {
16091
+ if (initialTemplateId) setTemplateId(initialTemplateId);
16092
+ }, [initialTemplateId]);
16093
+ useEffect7(() => {
16094
+ if (initialTemplateVersionId) setTemplateVersionId(initialTemplateVersionId);
16095
+ }, [initialTemplateVersionId]);
16096
+ useEffect7(() => {
16097
+ if (initialReviewStatus) setReviewStatus(initialReviewStatus);
16098
+ }, [initialReviewStatus]);
16099
+ const initialOutlineSlideCount = initialOutline?.slides?.length;
16100
+ useEffect7(() => {
16101
+ if (initialOutline) setOutline(initialOutline);
16102
+ }, [initialOutlineSlideCount]);
16048
16103
  const updateScale = useCallback4(() => {
16049
16104
  if (previewRef.current) setPreviewScale(previewRef.current.offsetWidth / 1280);
16050
16105
  }, []);
@@ -16108,13 +16163,21 @@ var PresentationJobCard = ({
16108
16163
  setProgress(data.progress);
16109
16164
  }
16110
16165
  if (newStatus === "complete" && data.output) {
16111
- const newTitle = data.output.title || initialTitle;
16112
- const newSlideCount = data.output.slide_count || 0;
16113
- const newFormats = data.output.formats || {};
16166
+ const output = data.output;
16167
+ const isOutlinePhase = output.review_status === "pending_outline_approval" || output.type === "presentation-outline";
16168
+ const newTitle = output.title || initialTitle;
16169
+ const newSlideCount = output.slide_count || 0;
16170
+ const newFormats = output.formats || {};
16114
16171
  setTitle(newTitle);
16115
16172
  setSlideCount(newSlideCount);
16116
16173
  setFormats(newFormats);
16117
- if (!hasNotifiedRef.current && onCompleteRef.current) {
16174
+ if (output.generation_mode) setGenerationMode(output.generation_mode);
16175
+ if (output.template_id) setTemplateId(output.template_id);
16176
+ if (output.template_version_id) setTemplateVersionId(output.template_version_id);
16177
+ if (output.review_status) setReviewStatus(output.review_status);
16178
+ if (output.outline) setOutline(output.outline);
16179
+ else if (output.deck_plan) setOutline(output.deck_plan);
16180
+ if (!isOutlinePhase && !hasNotifiedRef.current && onCompleteRef.current) {
16118
16181
  hasNotifiedRef.current = true;
16119
16182
  onCompleteRef.current({
16120
16183
  title: newTitle,
@@ -16134,6 +16197,184 @@ var PresentationJobCard = ({
16134
16197
  }
16135
16198
  // Transient fetch errors are ignored — the shared loop retries next tick.
16136
16199
  );
16200
+ const applyDeckOutput = useCallback4((out, opts) => {
16201
+ if (out.title) setTitle(out.title);
16202
+ if (out.slide_count !== void 0) setSlideCount(out.slide_count);
16203
+ if (out.formats) setFormats(out.formats);
16204
+ if (out.generation_mode) setGenerationMode(out.generation_mode);
16205
+ if (out.template_id) setTemplateId(out.template_id);
16206
+ setReviewStatus(out.review_status || "pending_review");
16207
+ setTemplateVersionId(opts?.regenerated ? "" : out.template_version_id || "");
16208
+ if (!opts?.regenerated && !hasNotifiedRef.current && onCompleteRef.current) {
16209
+ hasNotifiedRef.current = true;
16210
+ onCompleteRef.current({
16211
+ title: out.title ?? title,
16212
+ slide_count: out.slide_count ?? slideCount,
16213
+ formats: out.formats ?? formats
16214
+ });
16215
+ }
16216
+ }, [title, slideCount, formats]);
16217
+ const refetchSourceAndApply = useCallback4(async () => {
16218
+ if (!pollUrl) return;
16219
+ try {
16220
+ const headers = {};
16221
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
16222
+ const res = await fetch(pollUrl, { headers });
16223
+ if (!res.ok) return;
16224
+ const data = await res.json();
16225
+ if (data.output) applyDeckOutput(data.output);
16226
+ } catch {
16227
+ }
16228
+ }, [pollUrl, authToken, applyDeckOutput]);
16229
+ useSharedPoll(
16230
+ {
16231
+ key: outlineWritePollUrl,
16232
+ intervalMs: 3e3,
16233
+ fetcher: async () => {
16234
+ const headers = {};
16235
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
16236
+ const res = await fetch(outlineWritePollUrl, { headers });
16237
+ if (!res.ok) throw new Error(`poll ${res.status}`);
16238
+ return res.json();
16239
+ },
16240
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
16241
+ },
16242
+ (data) => {
16243
+ if (data.status === "complete") {
16244
+ setOutlineWritePollUrl(null);
16245
+ setApprovingOutline(false);
16246
+ void refetchSourceAndApply();
16247
+ } else if (data.status === "failed") {
16248
+ setApproveError(data.error || "Could not build the presentation");
16249
+ setOutlineWritePollUrl(null);
16250
+ setApprovingOutline(false);
16251
+ }
16252
+ }
16253
+ );
16254
+ useSharedPoll(
16255
+ {
16256
+ key: regenPollUrl,
16257
+ intervalMs: 3e3,
16258
+ fetcher: async () => {
16259
+ const headers = {};
16260
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
16261
+ const res = await fetch(regenPollUrl, { headers });
16262
+ if (!res.ok) throw new Error(`poll ${res.status}`);
16263
+ return res.json();
16264
+ },
16265
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
16266
+ },
16267
+ (data) => {
16268
+ if (data.status === "complete") {
16269
+ if (data.output) applyDeckOutput(data.output, { regenerated: true });
16270
+ setRegenPollUrl(null);
16271
+ setRegenerating(false);
16272
+ } else if (data.status === "failed") {
16273
+ setApproveError(data.error || "Could not switch template");
16274
+ setRegenPollUrl(null);
16275
+ setRegenerating(false);
16276
+ }
16277
+ }
16278
+ );
16279
+ const buildEditedOutline = () => {
16280
+ if (!outline || !Array.isArray(outline.slides)) return void 0;
16281
+ let changed = false;
16282
+ const slides = outline.slides.map((s, i) => {
16283
+ const edited = messageEdits[i];
16284
+ if (edited !== void 0 && edited !== s.message) {
16285
+ changed = true;
16286
+ return { ...s, message: edited };
16287
+ }
16288
+ return s;
16289
+ });
16290
+ return changed ? { ...outline, slides } : void 0;
16291
+ };
16292
+ const handleApproveOutline = async () => {
16293
+ if (!_job_id || approvingOutline) return;
16294
+ const endpoint = approveOutlineUrl || `/api/presentations/${_job_id}/approve-outline`;
16295
+ setApprovingOutline(true);
16296
+ setApproveError(null);
16297
+ try {
16298
+ const headers = { "Content-Type": "application/json" };
16299
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
16300
+ const editedOutline = buildEditedOutline();
16301
+ const res = await fetch(endpoint, {
16302
+ method: "POST",
16303
+ headers,
16304
+ body: JSON.stringify(editedOutline ? { outline: editedOutline } : {})
16305
+ });
16306
+ if (!res.ok) {
16307
+ const err = await res.json().catch(() => ({}));
16308
+ throw new Error(err.detail || err.error || `Approve failed (${res.status})`);
16309
+ }
16310
+ const data = await res.json();
16311
+ if (typeof data.job_id === "string" && data.job_id !== _job_id) {
16312
+ setOutlineWritePollUrl(deriveJobStatusUrl(data.job_id, pollUrl, regenerateUrl));
16313
+ return;
16314
+ }
16315
+ setApprovingOutline(false);
16316
+ void refetchSourceAndApply();
16317
+ } catch (e) {
16318
+ setApproveError(e instanceof Error ? e.message : "Could not build the presentation");
16319
+ setApprovingOutline(false);
16320
+ }
16321
+ };
16322
+ const handleApprove = async () => {
16323
+ if (!_job_id || approving) return;
16324
+ const endpoint = approveUrl || `/api/presentations/${_job_id}/approve-standard`;
16325
+ setApproving(true);
16326
+ setApproveError(null);
16327
+ try {
16328
+ const headers = { "Content-Type": "application/json" };
16329
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
16330
+ const res = await fetch(endpoint, {
16331
+ method: "POST",
16332
+ headers,
16333
+ body: JSON.stringify({ label: title })
16334
+ });
16335
+ if (!res.ok) {
16336
+ const err = await res.json().catch(() => ({}));
16337
+ throw new Error(err.detail || err.error || `Approve failed (${res.status})`);
16338
+ }
16339
+ const data = await res.json();
16340
+ setReviewStatus(data.review_status || "approved");
16341
+ if (data.template_version_id) setTemplateVersionId(data.template_version_id);
16342
+ if (data.template_id) setTemplateId(data.template_id);
16343
+ } catch (e) {
16344
+ setApproveError(e instanceof Error ? e.message : "Could not save the format");
16345
+ } finally {
16346
+ setApproving(false);
16347
+ }
16348
+ };
16349
+ const handleRegenerate = async () => {
16350
+ if (!_job_id || regenerating) return;
16351
+ const endpoint = regenerateUrl || `/api/presentations/${_job_id}/regenerate`;
16352
+ setRegenerating(true);
16353
+ setApproveError(null);
16354
+ try {
16355
+ const headers = { "Content-Type": "application/json" };
16356
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
16357
+ const res = await fetch(endpoint, {
16358
+ method: "POST",
16359
+ headers,
16360
+ body: JSON.stringify({})
16361
+ });
16362
+ if (!res.ok) {
16363
+ const err = await res.json().catch(() => ({}));
16364
+ throw new Error(err.detail || err.error || `Regenerate failed (${res.status})`);
16365
+ }
16366
+ const data = await res.json();
16367
+ if (typeof data.job_id === "string" && data.job_id !== _job_id) {
16368
+ setRegenPollUrl(deriveJobStatusUrl(data.job_id, pollUrl, regenerateUrl));
16369
+ return;
16370
+ }
16371
+ applyDeckOutput(data.output || data, { regenerated: true });
16372
+ setRegenerating(false);
16373
+ } catch (e) {
16374
+ setApproveError(e instanceof Error ? e.message : "Could not switch template");
16375
+ setRegenerating(false);
16376
+ }
16377
+ };
16137
16378
  const handleShare = async () => {
16138
16379
  const link = shareUrl ?? (typeof window !== "undefined" && _job_id ? `${window.location.origin}/p/${_job_id}` : formats.html_url);
16139
16380
  if (!link) return;
@@ -16254,8 +16495,85 @@ var PresentationJobCard = ({
16254
16495
  ] })
16255
16496
  ] }) }) });
16256
16497
  }
16498
+ if (status === "complete" && reviewStatus === "pending_outline_approval") {
16499
+ const slides = Array.isArray(outline?.slides) ? outline.slides : [];
16500
+ const building = Boolean(outlineWritePollUrl) || approvingOutline;
16501
+ return /* @__PURE__ */ jsx147("div", { className: cn("w-full", className), children: /* @__PURE__ */ jsxs108("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", style: t.root, children: [
16502
+ /* @__PURE__ */ jsx147(
16503
+ "div",
16504
+ {
16505
+ className: "h-[3px]",
16506
+ style: { background: accentGradient || "linear-gradient(90deg, #8B7D5E, #C0AE82, #DCC99B)" }
16507
+ }
16508
+ ),
16509
+ /* @__PURE__ */ jsxs108("div", { className: "p-5", children: [
16510
+ /* @__PURE__ */ jsxs108("div", { className: "flex items-start gap-3", children: [
16511
+ /* @__PURE__ */ jsx147(
16512
+ "div",
16513
+ {
16514
+ 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",
16515
+ style: { ...t.accentSubtle(0.1), ...t.accentBorder, ...t.accent },
16516
+ children: /* @__PURE__ */ jsx147(SlidesIcon, {})
16517
+ }
16518
+ ),
16519
+ /* @__PURE__ */ jsxs108("div", { className: "flex-1 min-w-0", children: [
16520
+ /* @__PURE__ */ jsx147("h3", { className: "text-sm font-semibold text-zinc-100 truncate leading-tight", style: t.text, children: title || "Presentation outline" }),
16521
+ /* @__PURE__ */ jsxs108("p", { className: "text-xs text-amber-400/90 mt-0.5", children: [
16522
+ "Review the slide outline",
16523
+ slides.length > 0 ? " \u2014 edit any slide message" : "",
16524
+ ", then build the deck."
16525
+ ] })
16526
+ ] })
16527
+ ] }),
16528
+ /* @__PURE__ */ jsxs108("div", { className: "mt-4 space-y-2", children: [
16529
+ slides.map((s, i) => /* @__PURE__ */ jsxs108("div", { className: "flex items-center gap-2", children: [
16530
+ /* @__PURE__ */ jsx147("span", { className: "text-xs text-zinc-600 w-5 text-right tabular-nums flex-shrink-0", children: s.n ?? i + 1 }),
16531
+ /* @__PURE__ */ jsxs108(
16532
+ "span",
16533
+ {
16534
+ className: "flex items-center gap-1 text-[10px] uppercase tracking-wide text-zinc-500 w-20 flex-shrink-0 truncate",
16535
+ title: s.type,
16536
+ children: [
16537
+ /* @__PURE__ */ jsx147(SlideTypeIcon, {}),
16538
+ s.type || "slide"
16539
+ ]
16540
+ }
16541
+ ),
16542
+ /* @__PURE__ */ jsx147(
16543
+ "input",
16544
+ {
16545
+ value: messageEdits[i] ?? s.message ?? "",
16546
+ onChange: (e) => setMessageEdits((prev) => ({ ...prev, [i]: e.target.value })),
16547
+ disabled: building,
16548
+ 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"
16549
+ }
16550
+ )
16551
+ ] }, i)),
16552
+ slides.length === 0 && /* @__PURE__ */ jsx147("p", { className: "text-xs text-zinc-500", children: "No outline slides were returned; you can still build the deck." })
16553
+ ] }),
16554
+ approveError && /* @__PURE__ */ jsx147("p", { className: "mt-3 text-xs text-red-400/90", children: approveError }),
16555
+ /* @__PURE__ */ jsx147("div", { className: "mt-4 flex items-center justify-end", children: /* @__PURE__ */ jsxs108(
16556
+ "button",
16557
+ {
16558
+ onClick: handleApproveOutline,
16559
+ disabled: building,
16560
+ title: "Approve this outline and build the full deck",
16561
+ 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",
16562
+ children: [
16563
+ /* @__PURE__ */ jsx147(CheckIcon, {}),
16564
+ /* @__PURE__ */ jsx147("span", { children: building ? "Building deck..." : "Approve & Build" })
16565
+ ]
16566
+ }
16567
+ ) })
16568
+ ] })
16569
+ ] }) });
16570
+ }
16257
16571
  const total = slideCount || 1;
16258
16572
  const hasHtml = Boolean(formats.html_url);
16573
+ const templateLabel = formatTemplateLabel(templateId);
16574
+ const isApproved = reviewStatus === "approved";
16575
+ const canApprove = !hideApproval && generationMode === "template" && reviewStatus === "pending_review" && Boolean(_job_id);
16576
+ const canRegenerate = canApprove;
16259
16577
  return /* @__PURE__ */ jsxs108(Fragment5, { children: [
16260
16578
  /* @__PURE__ */ jsx147("div", { className: cn("w-full", className), children: /* @__PURE__ */ jsxs108("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", style: t.root, children: [
16261
16579
  /* @__PURE__ */ jsx147("div", { className: "h-[3px]", style: { background: accentGradient || "linear-gradient(90deg, #8B7D5E, #C0AE82, #DCC99B)" } }),
@@ -16273,10 +16591,39 @@ var PresentationJobCard = ({
16273
16591
  /* @__PURE__ */ jsx147("h3", { className: "text-sm font-semibold text-zinc-100 truncate leading-tight", style: t.text, children: title }),
16274
16592
  /* @__PURE__ */ jsxs108("p", { className: "text-xs text-zinc-500 mt-0.5", style: t.muted, children: [
16275
16593
  slideCount ? `${slideCount} slides` : "Presentation",
16276
- " \xB7 Ready to export"
16277
- ] })
16594
+ " \xB7 Ready to export",
16595
+ templateLabel && ` \xB7 ${templateLabel}`
16596
+ ] }),
16597
+ generationMode === "template" && /* @__PURE__ */ jsx147("p", { className: "text-xs mt-1", children: isApproved ? /* @__PURE__ */ jsx147("span", { className: "text-emerald-400/90", children: "Approved \u2014 future decks use this format" }) : reviewStatus === "pending_review" ? /* @__PURE__ */ jsx147("span", { className: "text-amber-400/90", children: "Review this deck, then approve to lock the format" }) : null })
16278
16598
  ] }),
16279
- /* @__PURE__ */ jsxs108("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [
16599
+ /* @__PURE__ */ jsxs108("div", { className: "flex items-center gap-1.5 flex-shrink-0 flex-wrap justify-end", children: [
16600
+ canRegenerate && /* @__PURE__ */ jsxs108(
16601
+ "button",
16602
+ {
16603
+ onClick: handleRegenerate,
16604
+ disabled: regenerating || approving,
16605
+ title: "Try another design template with the same content",
16606
+ 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",
16607
+ children: [
16608
+ /* @__PURE__ */ jsx147(RefreshIcon, {}),
16609
+ /* @__PURE__ */ jsx147("span", { children: regenerating ? "Switching..." : "Regenerate" })
16610
+ ]
16611
+ }
16612
+ ),
16613
+ canApprove && /* @__PURE__ */ jsxs108(
16614
+ "button",
16615
+ {
16616
+ onClick: handleApprove,
16617
+ disabled: approving,
16618
+ title: "Approve and save this deck format for future runs",
16619
+ 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",
16620
+ children: [
16621
+ /* @__PURE__ */ jsx147(CheckIcon, {}),
16622
+ /* @__PURE__ */ jsx147("span", { children: approving ? "Saving..." : "Approve" })
16623
+ ]
16624
+ }
16625
+ ),
16626
+ isApproved && /* @__PURE__ */ jsx147("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__ */ jsx147(CheckIcon, {}) }),
16280
16627
  /* @__PURE__ */ jsx147(
16281
16628
  "button",
16282
16629
  {
@@ -16318,6 +16665,7 @@ var PresentationJobCard = ({
16318
16665
  )
16319
16666
  ] })
16320
16667
  ] }),
16668
+ approveError && /* @__PURE__ */ jsx147("p", { className: "mt-3 text-xs text-red-400/90", children: approveError }),
16321
16669
  hasHtml && /* @__PURE__ */ jsxs108("div", { className: "mt-4", children: [
16322
16670
  /* @__PURE__ */ jsxs108(
16323
16671
  "div",
@@ -16452,11 +16800,11 @@ var ShareIcon2 = () => /* @__PURE__ */ jsxs109("svg", { width: "14", height: "14
16452
16800
  /* @__PURE__ */ jsx148("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
16453
16801
  ] });
16454
16802
  var CheckIcon2 = () => /* @__PURE__ */ jsx148("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx148("polyline", { points: "20 6 9 17 4 12" }) });
16455
- var RefreshIcon = () => /* @__PURE__ */ jsxs109("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
16803
+ var RefreshIcon2 = () => /* @__PURE__ */ jsxs109("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [
16456
16804
  /* @__PURE__ */ jsx148("polyline", { points: "23 4 23 10 17 10" }),
16457
16805
  /* @__PURE__ */ jsx148("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
16458
16806
  ] });
16459
- function formatTemplateLabel(templateId) {
16807
+ function formatTemplateLabel2(templateId) {
16460
16808
  if (!templateId) return null;
16461
16809
  return templateId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
16462
16810
  }
@@ -16503,6 +16851,10 @@ var FullscreenPreviewModal = ({ url, title, onClose }) => {
16503
16851
  ) })
16504
16852
  ] });
16505
16853
  };
16854
+ function deriveJobStatusUrl2(newJobId, pollUrl, regenerateUrl) {
16855
+ const base = pollUrl?.replace(/\/api\/jobs\/[^/]+\/status.*$/, "") ?? regenerateUrl?.replace(/\/api\/reports\/[^/]+\/regenerate.*$/, "") ?? "";
16856
+ return `${base}/api/jobs/${newJobId}/status`;
16857
+ }
16506
16858
  var ResearchReportJobCard = (props) => {
16507
16859
  const {
16508
16860
  job_id,
@@ -16520,6 +16872,7 @@ var ResearchReportJobCard = (props) => {
16520
16872
  template_id: initialTemplateId,
16521
16873
  template_version_id: initialTemplateVersionId,
16522
16874
  review_status: initialReviewStatus,
16875
+ outline: initialOutline,
16523
16876
  theme: initialTheme,
16524
16877
  error: initialError,
16525
16878
  progress: initialProgress,
@@ -16527,7 +16880,9 @@ var ResearchReportJobCard = (props) => {
16527
16880
  authToken,
16528
16881
  shareUrl,
16529
16882
  approveUrl,
16883
+ approveOutlineUrl,
16530
16884
  regenerateUrl,
16885
+ hideApproval = false,
16531
16886
  className,
16532
16887
  onComplete,
16533
16888
  onFailed,
@@ -16550,6 +16905,7 @@ var ResearchReportJobCard = (props) => {
16550
16905
  const [reviewStatus, setReviewStatus] = useState11(
16551
16906
  initialReviewStatus || (initialHtmlUrl ? "pending_review" : "")
16552
16907
  );
16908
+ const [outline, setOutline] = useState11(initialOutline);
16553
16909
  const [theme, setTheme] = useState11(initialTheme || DEFAULT_THEME);
16554
16910
  const [error, setError] = useState11(initialError);
16555
16911
  const [progress, setProgress] = useState11(initialProgress);
@@ -16559,6 +16915,10 @@ var ResearchReportJobCard = (props) => {
16559
16915
  const [approving, setApproving] = useState11(false);
16560
16916
  const [regenerating, setRegenerating] = useState11(false);
16561
16917
  const [approveError, setApproveError] = useState11(null);
16918
+ const [headingEdits, setHeadingEdits] = useState11({});
16919
+ const [approvingOutline, setApprovingOutline] = useState11(false);
16920
+ const [outlineWritePollUrl, setOutlineWritePollUrl] = useState11(null);
16921
+ const [regenPollUrl, setRegenPollUrl] = useState11(null);
16562
16922
  const previewRef = useRef7(null);
16563
16923
  const onCompleteRef = useRef7(onComplete);
16564
16924
  const onFailedRef = useRef7(onFailed);
@@ -16587,6 +16947,10 @@ var ResearchReportJobCard = (props) => {
16587
16947
  useEffect8(() => {
16588
16948
  if (initialReviewStatus) setReviewStatus(initialReviewStatus);
16589
16949
  }, [initialReviewStatus]);
16950
+ const initialOutlineSectionCount = initialOutline?.sections?.length;
16951
+ useEffect8(() => {
16952
+ if (initialOutline) setOutline(initialOutline);
16953
+ }, [initialOutlineSectionCount]);
16590
16954
  useEffect8(() => {
16591
16955
  if (initialDepth) setDepth(initialDepth);
16592
16956
  }, [initialDepth]);
@@ -16652,6 +17016,7 @@ var ResearchReportJobCard = (props) => {
16652
17016
  }
16653
17017
  if (newStatus === "complete" && data.output) {
16654
17018
  const output = data.output;
17019
+ const isOutlinePhase = output.review_status === "pending_outline_approval" || output.type === "research-report-outline";
16655
17020
  setTitle(output.title || initialTitle);
16656
17021
  setDepth(output.depth || "");
16657
17022
  setSectionCount(output.section_count || 0);
@@ -16663,10 +17028,11 @@ var ResearchReportJobCard = (props) => {
16663
17028
  if (output.template_id) setTemplateId(output.template_id);
16664
17029
  if (output.template_version_id) setTemplateVersionId(output.template_version_id);
16665
17030
  if (output.review_status) setReviewStatus(output.review_status);
17031
+ if (output.outline) setOutline(output.outline);
16666
17032
  if (output.theme) {
16667
17033
  setTheme(output.theme);
16668
17034
  }
16669
- if (!hasNotifiedRef.current && onCompleteRef.current) {
17035
+ if (!isOutlinePhase && !hasNotifiedRef.current && onCompleteRef.current) {
16670
17036
  hasNotifiedRef.current = true;
16671
17037
  onCompleteRef.current(output);
16672
17038
  }
@@ -16682,6 +17048,156 @@ var ResearchReportJobCard = (props) => {
16682
17048
  }
16683
17049
  // Transient fetch errors are ignored — the shared loop retries next tick.
16684
17050
  );
17051
+ const applyRegeneratedOutput = useCallback5(
17052
+ (out) => {
17053
+ if (out.html_url) setHtmlUrl(out.html_url);
17054
+ if (out.template_id) setTemplateId(out.template_id);
17055
+ setReviewStatus("pending_review");
17056
+ setTemplateVersionId("");
17057
+ if (out.section_count !== void 0) setSectionCount(out.section_count);
17058
+ if (out.title) setTitle(out.title);
17059
+ if (out.depth) setDepth(out.depth);
17060
+ if (out.theme) setTheme(out.theme);
17061
+ if (!onCompleteRef.current) return;
17062
+ onCompleteRef.current({
17063
+ title: out.title ?? title,
17064
+ depth: out.depth ?? depth,
17065
+ section_count: out.section_count ?? sectionCount,
17066
+ source_count: out.source_count ?? sourceCount,
17067
+ word_count: out.word_count ?? wordCount,
17068
+ executive_summary: out.executive_summary ?? summary,
17069
+ html_url: out.html_url ?? htmlUrl,
17070
+ generation_mode: "template",
17071
+ template_id: out.template_id ?? templateId,
17072
+ template_version_id: void 0,
17073
+ review_status: "pending_review",
17074
+ theme: out.theme ?? theme
17075
+ });
17076
+ },
17077
+ [title, depth, sectionCount, sourceCount, wordCount, summary, htmlUrl, templateId, theme]
17078
+ );
17079
+ useSharedPoll(
17080
+ {
17081
+ key: regenPollUrl,
17082
+ intervalMs: 3e3,
17083
+ fetcher: async () => {
17084
+ const headers = {};
17085
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
17086
+ const res = await fetch(regenPollUrl, { headers });
17087
+ if (!res.ok) throw new Error(`poll ${res.status}`);
17088
+ return res.json();
17089
+ },
17090
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
17091
+ },
17092
+ (data) => {
17093
+ if (data.status === "complete") {
17094
+ if (data.output) applyRegeneratedOutput(data.output);
17095
+ setRegenPollUrl(null);
17096
+ setRegenerating(false);
17097
+ } else if (data.status === "failed") {
17098
+ setApproveError(data.error || "Could not switch template");
17099
+ setRegenPollUrl(null);
17100
+ setRegenerating(false);
17101
+ }
17102
+ }
17103
+ );
17104
+ const refetchSourceAndApply = useCallback5(async () => {
17105
+ if (!pollUrl) return;
17106
+ try {
17107
+ const headers = {};
17108
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
17109
+ const res = await fetch(pollUrl, { headers });
17110
+ if (!res.ok) return;
17111
+ const data = await res.json();
17112
+ const out = data.output;
17113
+ if (!out) return;
17114
+ if (out.title) setTitle(out.title);
17115
+ if (out.depth) setDepth(out.depth);
17116
+ if (out.section_count !== void 0) setSectionCount(out.section_count);
17117
+ if (out.source_count !== void 0) setSourceCount(out.source_count);
17118
+ if (out.word_count !== void 0) setWordCount(out.word_count);
17119
+ if (out.executive_summary) setSummary(out.executive_summary);
17120
+ if (out.html_url) setHtmlUrl(out.html_url);
17121
+ if (out.generation_mode) setGenerationMode(out.generation_mode);
17122
+ if (out.template_id) setTemplateId(out.template_id);
17123
+ setTemplateVersionId(out.template_version_id || "");
17124
+ setReviewStatus(out.review_status || "pending_review");
17125
+ if (out.theme) setTheme(out.theme);
17126
+ if (!hasNotifiedRef.current && onCompleteRef.current) {
17127
+ hasNotifiedRef.current = true;
17128
+ onCompleteRef.current(out);
17129
+ }
17130
+ } catch {
17131
+ }
17132
+ }, [pollUrl, authToken]);
17133
+ useSharedPoll(
17134
+ {
17135
+ key: outlineWritePollUrl,
17136
+ intervalMs: 3e3,
17137
+ fetcher: async () => {
17138
+ const headers = {};
17139
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
17140
+ const res = await fetch(outlineWritePollUrl, { headers });
17141
+ if (!res.ok) throw new Error(`poll ${res.status}`);
17142
+ return res.json();
17143
+ },
17144
+ shouldContinue: (data) => data.status !== "complete" && data.status !== "failed"
17145
+ },
17146
+ (data) => {
17147
+ if (data.status === "complete") {
17148
+ setOutlineWritePollUrl(null);
17149
+ setApprovingOutline(false);
17150
+ void refetchSourceAndApply();
17151
+ } else if (data.status === "failed") {
17152
+ setApproveError(data.error || "Could not build the report");
17153
+ setOutlineWritePollUrl(null);
17154
+ setApprovingOutline(false);
17155
+ }
17156
+ }
17157
+ );
17158
+ const buildEditedOutline = () => {
17159
+ if (!outline || !Array.isArray(outline.sections)) return void 0;
17160
+ let changed = false;
17161
+ const sections = outline.sections.map((s, i) => {
17162
+ const edited = headingEdits[i];
17163
+ if (edited !== void 0 && edited !== s.heading) {
17164
+ changed = true;
17165
+ return { ...s, heading: edited };
17166
+ }
17167
+ return s;
17168
+ });
17169
+ return changed ? { ...outline, sections } : void 0;
17170
+ };
17171
+ const handleApproveOutline = async () => {
17172
+ if (!job_id || approvingOutline) return;
17173
+ const endpoint = approveOutlineUrl || `/api/reports/${job_id}/approve-outline`;
17174
+ setApprovingOutline(true);
17175
+ setApproveError(null);
17176
+ try {
17177
+ const headers = { "Content-Type": "application/json" };
17178
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
17179
+ const editedOutline = buildEditedOutline();
17180
+ const res = await fetch(endpoint, {
17181
+ method: "POST",
17182
+ headers,
17183
+ body: JSON.stringify(editedOutline ? { outline: editedOutline } : {})
17184
+ });
17185
+ if (!res.ok) {
17186
+ const err = await res.json().catch(() => ({}));
17187
+ throw new Error(err.detail || err.error || `Approve failed (${res.status})`);
17188
+ }
17189
+ const data = await res.json();
17190
+ if (typeof data.job_id === "string" && data.job_id !== job_id) {
17191
+ setOutlineWritePollUrl(deriveJobStatusUrl2(data.job_id, pollUrl, regenerateUrl));
17192
+ return;
17193
+ }
17194
+ setApprovingOutline(false);
17195
+ void refetchSourceAndApply();
17196
+ } catch (e) {
17197
+ setApproveError(e instanceof Error ? e.message : "Could not build the report");
17198
+ setApprovingOutline(false);
17199
+ }
17200
+ };
16685
17201
  const formatWordCount = (count) => {
16686
17202
  if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
16687
17203
  return count.toString();
@@ -16787,6 +17303,61 @@ var ResearchReportJobCard = (props) => {
16787
17303
  ] })
16788
17304
  ] }) }) });
16789
17305
  }
17306
+ if (status === "complete" && reviewStatus === "pending_outline_approval") {
17307
+ const sections = Array.isArray(outline?.sections) ? outline.sections : [];
17308
+ const building = Boolean(outlineWritePollUrl) || approvingOutline;
17309
+ return /* @__PURE__ */ jsx148("div", { className: cn("w-full", className), children: /* @__PURE__ */ jsxs109("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", children: [
17310
+ /* @__PURE__ */ jsx148(
17311
+ "div",
17312
+ {
17313
+ className: "h-[3px]",
17314
+ style: { background: "linear-gradient(90deg, #8B7D5E, #C0AE82, #DCC99B)" }
17315
+ }
17316
+ ),
17317
+ /* @__PURE__ */ jsxs109("div", { className: "p-5", children: [
17318
+ /* @__PURE__ */ jsxs109("div", { className: "flex items-start gap-3", children: [
17319
+ /* @__PURE__ */ jsx148("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__ */ jsx148(DocumentIcon, {}) }),
17320
+ /* @__PURE__ */ jsxs109("div", { className: "flex-1 min-w-0", children: [
17321
+ /* @__PURE__ */ jsx148("h3", { className: "text-sm font-semibold text-zinc-100 truncate leading-tight", children: title || "Report outline" }),
17322
+ /* @__PURE__ */ jsxs109("p", { className: "text-xs text-amber-400/90 mt-0.5", children: [
17323
+ "Review the outline",
17324
+ sections.length > 0 ? " \u2014 edit any heading" : "",
17325
+ ", then build the full report."
17326
+ ] })
17327
+ ] })
17328
+ ] }),
17329
+ /* @__PURE__ */ jsxs109("div", { className: "mt-4 space-y-2", children: [
17330
+ sections.map((s, i) => /* @__PURE__ */ jsxs109("div", { className: "flex items-center gap-2", children: [
17331
+ /* @__PURE__ */ jsx148("span", { className: "text-xs text-zinc-600 w-5 text-right tabular-nums flex-shrink-0", children: i + 1 }),
17332
+ /* @__PURE__ */ jsx148(
17333
+ "input",
17334
+ {
17335
+ value: headingEdits[i] ?? s.heading ?? "",
17336
+ onChange: (e) => setHeadingEdits((prev) => ({ ...prev, [i]: e.target.value })),
17337
+ disabled: building,
17338
+ 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"
17339
+ }
17340
+ )
17341
+ ] }, i)),
17342
+ sections.length === 0 && /* @__PURE__ */ jsx148("p", { className: "text-xs text-zinc-500", children: "No outline sections were returned; you can still build the report." })
17343
+ ] }),
17344
+ approveError && /* @__PURE__ */ jsx148("p", { className: "mt-3 text-xs text-red-400/90", children: approveError }),
17345
+ /* @__PURE__ */ jsx148("div", { className: "mt-4 flex items-center justify-end", children: /* @__PURE__ */ jsxs109(
17346
+ "button",
17347
+ {
17348
+ onClick: handleApproveOutline,
17349
+ disabled: building,
17350
+ title: "Approve this outline and build the full report",
17351
+ 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",
17352
+ children: [
17353
+ /* @__PURE__ */ jsx148(CheckIcon2, {}),
17354
+ /* @__PURE__ */ jsx148("span", { children: building ? "Building report..." : "Approve & Build" })
17355
+ ]
17356
+ }
17357
+ ) })
17358
+ ] })
17359
+ ] }) });
17360
+ }
16790
17361
  if (status === "complete" && !hasHTML) {
16791
17362
  return /* @__PURE__ */ jsx148("div", { className: cn("w-full", className), children: /* @__PURE__ */ jsxs109("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", children: [
16792
17363
  /* @__PURE__ */ jsx148(
@@ -16850,8 +17421,8 @@ var ResearchReportJobCard = (props) => {
16850
17421
  window.open(link, "_blank", "noopener,noreferrer");
16851
17422
  }
16852
17423
  };
16853
- const canApprove = generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
16854
- const canRegenerate = generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
17424
+ const canApprove = !hideApproval && generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
17425
+ const canRegenerate = !hideApproval && generationMode === "template" && reviewStatus === "pending_review" && Boolean(job_id);
16855
17426
  const canShare = Boolean(hasHTML || job_id) && (generationMode !== "template" || reviewStatus !== "pending_review");
16856
17427
  const handleApprove = async () => {
16857
17428
  if (!job_id || approving) return;
@@ -16915,36 +17486,18 @@ var ResearchReportJobCard = (props) => {
16915
17486
  throw new Error(err.detail || err.error || `Regenerate failed (${res.status})`);
16916
17487
  }
16917
17488
  const data = await res.json();
16918
- const out = data.output || data;
16919
- if (out.html_url) setHtmlUrl(out.html_url);
16920
- if (data.template_id || out.template_id) setTemplateId(data.template_id || out.template_id);
16921
- setReviewStatus("pending_review");
16922
- setTemplateVersionId("");
16923
- if (out.section_count !== void 0) setSectionCount(out.section_count);
16924
- if (out.title) setTitle(out.title);
16925
- if (onCompleteRef.current) {
16926
- onCompleteRef.current({
16927
- title: out.title || title,
16928
- depth,
16929
- section_count: out.section_count ?? sectionCount,
16930
- source_count: sourceCount,
16931
- word_count: wordCount,
16932
- executive_summary: summary,
16933
- html_url: out.html_url || htmlUrl,
16934
- generation_mode: "template",
16935
- template_id: data.template_id || out.template_id || templateId,
16936
- template_version_id: void 0,
16937
- review_status: "pending_review",
16938
- theme
16939
- });
17489
+ if (typeof data.job_id === "string" && data.job_id !== job_id) {
17490
+ setRegenPollUrl(deriveJobStatusUrl2(data.job_id, pollUrl, regenerateUrl));
17491
+ return;
16940
17492
  }
17493
+ applyRegeneratedOutput(data.output || data);
17494
+ setRegenerating(false);
16941
17495
  } catch (e) {
16942
17496
  setApproveError(e instanceof Error ? e.message : "Could not switch template");
16943
- } finally {
16944
17497
  setRegenerating(false);
16945
17498
  }
16946
17499
  };
16947
- const templateLabel = formatTemplateLabel(templateId);
17500
+ const templateLabel = formatTemplateLabel2(templateId);
16948
17501
  const isApproved = reviewStatus === "approved";
16949
17502
  return /* @__PURE__ */ jsxs109(Fragment6, { children: [
16950
17503
  /* @__PURE__ */ jsx148("div", { className: cn("w-full", className), children: /* @__PURE__ */ jsxs109("div", { className: "bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden", children: [
@@ -16993,7 +17546,7 @@ var ResearchReportJobCard = (props) => {
16993
17546
  title: "Try another report template with the same data",
16994
17547
  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",
16995
17548
  children: [
16996
- /* @__PURE__ */ jsx148(RefreshIcon, {}),
17549
+ /* @__PURE__ */ jsx148(RefreshIcon2, {}),
16997
17550
  /* @__PURE__ */ jsx148("span", { children: regenerating ? "Switching..." : "Regenerate" })
16998
17551
  ]
16999
17552
  }