@aglyn/plugins-ai 1.0.0-beta.293 → 1.0.0

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.
Files changed (64) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +17 -3
  3. package/src/lib/components/ai-job-links.js +32 -5
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-job-requests.d.ts +9 -0
  6. package/src/lib/components/ai-job-requests.js +34 -0
  7. package/src/lib/components/ai-job-requests.js.map +1 -1
  8. package/src/lib/components/ai-site-build-page.component.js +28 -2
  9. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  10. package/src/lib/components/assist-edit-canvas.d.ts +12 -6
  11. package/src/lib/components/assist-edit-canvas.js +86 -36
  12. package/src/lib/components/assist-edit-canvas.js.map +1 -1
  13. package/src/lib/components/assist-edit-card.component.js +9 -8
  14. package/src/lib/components/assist-edit-card.component.js.map +1 -1
  15. package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
  16. package/src/lib/components/assist-jobs-drawer.component.js +20 -6
  17. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  18. package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
  19. package/src/lib/components/assist-page-change-card.component.js +166 -0
  20. package/src/lib/components/assist-page-change-card.component.js.map +1 -0
  21. package/src/lib/components/assist-panel.component.d.ts +105 -1
  22. package/src/lib/components/assist-panel.component.js +242 -36
  23. package/src/lib/components/assist-panel.component.js.map +1 -1
  24. package/src/lib/jobs/ai-job-edit-step.d.ts +82 -16
  25. package/src/lib/jobs/ai-job-edit-step.js +209 -62
  26. package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-navigation-step.js +4 -2
  28. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  29. package/src/lib/model/ai-edit-job.d.ts +33 -16
  30. package/src/lib/model/ai-edit-job.js +50 -34
  31. package/src/lib/model/ai-edit-job.js.map +1 -1
  32. package/src/lib/model/ai-job-activity.d.ts +6 -11
  33. package/src/lib/model/ai-job-activity.js +8 -15
  34. package/src/lib/model/ai-job-activity.js.map +1 -1
  35. package/src/lib/model/ai-job-notice.js +3 -1
  36. package/src/lib/model/ai-job-notice.js.map +1 -1
  37. package/src/lib/model/ai-jobs.types.d.ts +6 -0
  38. package/src/lib/model/ai-jobs.types.js.map +1 -1
  39. package/src/lib/model/ai-site-inventory.d.ts +6 -0
  40. package/src/lib/model/ai-site-inventory.js.map +1 -1
  41. package/src/lib/model/assist-edit.d.ts +19 -1
  42. package/src/lib/model/assist-edit.js +7 -0
  43. package/src/lib/model/assist-edit.js.map +1 -1
  44. package/src/lib/model/assist-page-change.d.ts +100 -0
  45. package/src/lib/model/assist-page-change.js +107 -0
  46. package/src/lib/model/assist-page-change.js.map +1 -0
  47. package/src/lib/runtime/ai-eval.js +3 -1
  48. package/src/lib/runtime/ai-eval.js.map +1 -1
  49. package/src/lib/runtime/ai-node-tree.d.ts +5 -0
  50. package/src/lib/runtime/ai-node-tree.js +18 -3
  51. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  52. package/src/lib/runtime/site-inventory.d.ts +8 -1
  53. package/src/lib/runtime/site-inventory.js +18 -0
  54. package/src/lib/runtime/site-inventory.js.map +1 -1
  55. package/src/lib/server/assist-build.js +2 -0
  56. package/src/lib/server/assist-build.js.map +1 -1
  57. package/src/lib/server/assist-chat.js +56 -3
  58. package/src/lib/server/assist-chat.js.map +1 -1
  59. package/src/lib/server/assist-edit.d.ts +61 -4
  60. package/src/lib/server/assist-edit.js +248 -38
  61. package/src/lib/server/assist-edit.js.map +1 -1
  62. package/src/lib/server/assist-page-change.d.ts +62 -0
  63. package/src/lib/server/assist-page-change.js +140 -0
  64. package/src/lib/server/assist-page-change.js.map +1 -0
@@ -25,7 +25,7 @@ import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
25
25
  import { BetaChip } from "@aglyn/shared-ui-jsx/components/beta-chip.component";
26
26
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
27
27
  import { Alert, Badge, Box, Button, Chip, CircularProgress, Collapse, Drawer, Fab, IconButton, Link, Paper, Stack, TextField, Tooltip, Typography } from "@mui/material";
28
- import { usePathname } from "next/navigation";
28
+ import { usePathname, useRouter } from "next/navigation";
29
29
  import { Fragment, memo, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
30
30
  import { useUser } from "@aglyn/tenant-feature-instance";
31
31
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
@@ -35,11 +35,14 @@ import { AssistEditCard } from "./assist-edit-card.component.js";
35
35
  import { AssistBuildCard } from "./assist-build-card.component.js";
36
36
  import { isAssistBuildProposal } from "../model/assist-build.js";
37
37
  import { assistBuildDraftLinks, isAssistFollowUpDue, isLiveAssistFollowUp } from "../model/assist-follow-up.js";
38
- import { startAssistBuildRequest } from "./ai-job-requests.js";
38
+ import { cancelAiJobRequest, startAssistBuildRequest, startAssistPageChangeRequest } from "./ai-job-requests.js";
39
+ import { assistPageChangeDraftVersion, assistPageChangeHistoryNote, isAssistPageChangeProposal } from "../model/assist-page-change.js";
40
+ import { AssistPageChangeCard } from "./assist-page-change-card.component.js";
39
41
  import { aiInsightSurfaceForPath } from "../model/ai-insight.js";
40
42
  import { AssistJobsDrawer } from "./assist-jobs-drawer.component.js";
41
43
  import { AI_JOB_LINK_PARAM, openAiJobs, useAiJobsInFlight, useAiJobsOpenRequest } from "./ai-jobs-store.js";
42
- import { aiJobsActivity, aiJobsLauncherLabel } from "../model/ai-job-activity.js";
44
+ import { aiJobPhase, aiJobsActivity, aiJobsLauncherLabel } from "../model/ai-job-activity.js";
45
+ import { AI_SITE_BUILD_HREF } from "../model/ai-job-notice.js";
43
46
  import { AiModelSelector } from "./ai-model-selector.component.js";
44
47
  import { AiUsageStrip } from "./ai-usage-strip.component.js";
45
48
  import { useAiModelChoice } from "./use-ai-model-choice.js";
@@ -47,6 +50,15 @@ import { followAiJobsForUsageMeter, refreshAiUsageMeterFree } from "./ai-usage-m
47
50
  import { readAiUsageMeter, usePublishAiUsageMeter } from "./use-ai-usage-meter.js";
48
51
  import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
49
52
  import { HelpTip } from "@aglyn/shared-ui-jsx";
53
+ /**
54
+ * A turn as the next question's history carries it (AGL-4116): its words, and
55
+ * what became of the change it made, which the words alone never say.
56
+ */ function assistHistoryText(message) {
57
+ var _assistPageChangeHistoryNote;
58
+ var _message_editOutcome;
59
+ const note = (_assistPageChangeHistoryNote = assistPageChangeHistoryNote(message)) != null ? _assistPageChangeHistoryNote : message.edit && ((_message_editOutcome = message.editOutcome) == null ? void 0 : _message_editOutcome.kind) === 'applied' ? `(Applied to the open canvas as an unsaved change: ${message.edit.summary})` : null;
60
+ return note ? `${message.text}\n\n${note}` : message.text;
61
+ }
50
62
  /** The key a follow-up waiting for its canvas is kept under, per org (AGL-3616). */ const followUpKey = (orgId)=>`aglyn-assist-follow-up:${orgId}`;
51
63
  function loadFollowUp(orgId) {
52
64
  if (!orgId || typeof sessionStorage === 'undefined') return null;
@@ -80,28 +92,69 @@ function saveFollowUp(orgId, followUp) {
80
92
  ] : []).reverse();
81
93
  return assistBuildDraftLinks(jobs, orgSlug);
82
94
  }
95
+ /**
96
+ * The builds in a thread whose plan is still waiting on the person
97
+ * (AGL-4116): queued, being planned, or planned and not yet confirmed. A new
98
+ * plan in the same thread replaces them, so a conversation that keeps
99
+ * changing a plan holds one plan, not one per message.
100
+ */ export function assistUnconfirmedBuilds(messages) {
101
+ return messages.flatMap((message, index)=>{
102
+ const job = message.build && !message.buildReplaced ? message.buildJob : null;
103
+ if (!job) return [];
104
+ const phase = aiJobPhase(job);
105
+ return phase === 'queued' || phase === 'planning' || phase === 'plan-ready' ? [
106
+ {
107
+ index,
108
+ jobId: job.id
109
+ }
110
+ ] : [];
111
+ });
112
+ }
113
+ /** What a plan a later one replaced says in its card's place (AGL-4116). */ export const ASSIST_BUILD_REPLACED_COPY = 'Replaced by the updated plan below. Nothing from this one was built.';
83
114
  /** What a stored build that never got its job says, rather than planning forever. */ const BUILD_NOT_STARTED = 'This build was not started. Ask again to plan it.';
115
+ /** What a stored page change that never got its job says (AGL-4116). */ const PAGE_CHANGE_NOT_STARTED = 'This change was not started. Ask again to make it.';
84
116
  const HISTORY_TURNS_SENT = 12;
85
- const storageKey = (orgId)=>`aglyn-assist:${orgId}`;
86
- function loadThread(orgId) {
87
- if (!orgId || typeof sessionStorage === 'undefined') return [];
117
+ /**
118
+ * The thread a question belongs to: one per site, and one for the
119
+ * workspace's own pages (AGL-4116). A workspace-wide thread carried one
120
+ * site's request into a build on another site, because the history that
121
+ * rides each question is the thread's last turns.
122
+ */ export function assistThreadKey(orgId, hostId) {
123
+ return hostId ? `aglyn-assist:${orgId}:${hostId}` : `aglyn-assist:${orgId}`;
124
+ }
125
+ const NO_MESSAGES = [];
126
+ /**
127
+ * The AI jobs page of the site a console page is on (AGL-4116), or `null`
128
+ * off a site: `/{orgSlug}/hosts/{site}/ai-jobs`, the site named as the
129
+ * address names it.
130
+ */ export function assistSiteAiJobsHref(pathname, orgSlug, hostId) {
131
+ if (!pathname || !orgSlug || !hostId) return null;
132
+ const parts = pathname.split('/').filter(Boolean);
133
+ const site = parts[0] === orgSlug && parts[1] === 'hosts' ? parts[2] : undefined;
134
+ return site ? `/${orgSlug}/hosts/${site}${AI_SITE_BUILD_HREF}` : null;
135
+ }
136
+ function loadThread(key) {
137
+ if (typeof sessionStorage === 'undefined') return [];
88
138
  try {
89
- const raw = sessionStorage.getItem(storageKey(orgId));
139
+ const raw = sessionStorage.getItem(key);
90
140
  const parsed = raw ? JSON.parse(raw) : [];
91
141
  if (!Array.isArray(parsed)) return [];
92
142
  // A build whose job was still being created when the page went away is
93
143
  // not started again from a stored thread (AGL-3616): it says so instead.
144
+ // A page change likewise (AGL-4116).
94
145
  return parsed.map((message)=>message.build && !message.buildJob && !message.buildNotice ? _extends({}, message, {
95
146
  buildNotice: BUILD_NOT_STARTED
147
+ }) : message.pageChange && !message.pageChangeJob && !message.pageChangeNotice ? _extends({}, message, {
148
+ pageChangeNotice: PAGE_CHANGE_NOT_STARTED
96
149
  }) : message);
97
150
  } catch (unused) {
98
151
  return [];
99
152
  }
100
153
  }
101
- function saveThread(orgId, messages) {
102
- if (!orgId || typeof sessionStorage === 'undefined') return;
154
+ function saveThread(key, messages) {
155
+ if (typeof sessionStorage === 'undefined') return;
103
156
  try {
104
- sessionStorage.setItem(storageKey(orgId), JSON.stringify(messages));
157
+ sessionStorage.setItem(key, JSON.stringify(messages));
105
158
  } catch (unused) {
106
159
  // Quota/full storage never breaks chat.
107
160
  }
@@ -523,7 +576,25 @@ export function AssistPanelComponent(props) {
523
576
  const { enqueueSnackbar } = useSnackbar();
524
577
  const [open, setOpen] = useState(false);
525
578
  const [busy, setBusy] = useState(false);
526
- const [messages, setMessages] = useState([]);
579
+ // Every thread this panel has opened, by its key (AGL-4116): the one on
580
+ // screen is this site's, and an answer still streaming lands in the thread
581
+ // it was asked in, even after the page moved to another site.
582
+ const threadKey = scopedOrgId ? assistThreadKey(scopedOrgId, hostId) : null;
583
+ const [threads, setThreads] = useState({});
584
+ const messages = threadKey && threads[threadKey] || NO_MESSAGES;
585
+ const updateThread = useCallback((key, update)=>{
586
+ if (!key) return;
587
+ setThreads((prior)=>{
588
+ var _prior_key;
589
+ return _extends({}, prior, {
590
+ [key]: update((_prior_key = prior[key]) != null ? _prior_key : loadThread(key))
591
+ });
592
+ });
593
+ }, []);
594
+ const setMessages = useCallback((update)=>updateThread(threadKey, update), [
595
+ threadKey,
596
+ updateThread
597
+ ]);
527
598
  const [quota, setQuota] = useState(null);
528
599
  const scrollRef = useRef(null);
529
600
  // The model switch and the usage strip (AGL-2942): the pick rides the
@@ -540,22 +611,40 @@ export function AssistPanelComponent(props) {
540
611
  // the one open.
541
612
  const editorRevision = useSyncExternalStore(subscribeEditorSessions, editorSessionsRevision, editorSessionsRevision);
542
613
  const editorSession = openEditorSession();
543
- // Thread is per-org, session-persisted; switching orgs swaps threads.
544
- // Keyed on the SCOPED org (AGL-1934): gating only the render would still
545
- // load — and, on the next keystroke, save — a thread under the fallback
546
- // org's key on every mount of a page that named no workspace.
614
+ // A thread per site, session-persisted; moving to another site or org
615
+ // swaps threads (AGL-4116). Keyed on the SCOPED org (AGL-1934): gating only
616
+ // the render would still load — and, on the next keystroke, save — a
617
+ // thread under the fallback org's key on every mount of a page that named
618
+ // no workspace.
619
+ useEffect(()=>{
620
+ if (!threadKey) return;
621
+ setThreads((prior)=>threadKey in prior ? prior : _extends({}, prior, {
622
+ [threadKey]: loadThread(threadKey)
623
+ }));
624
+ }, [
625
+ threadKey
626
+ ]);
547
627
  useEffect(()=>{
548
- setMessages(loadThread(scopedOrgId));
549
628
  setQuota(null);
550
629
  }, [
551
630
  scopedOrgId
552
631
  ]);
632
+ // Each thread is written back when it changes, the one on screen or one an
633
+ // answer is still streaming into.
634
+ const savedThreads = useRef({});
635
+ useEffect(()=>{
636
+ for (const [key, list] of Object.entries(threads)){
637
+ if (savedThreads.current[key] === list) continue;
638
+ savedThreads.current[key] = list;
639
+ saveThread(key, list);
640
+ }
641
+ }, [
642
+ threads
643
+ ]);
553
644
  useEffect(()=>{
554
- saveThread(scopedOrgId, messages);
555
645
  const node = scrollRef.current;
556
646
  if (node) node.scrollTop = node.scrollHeight;
557
647
  }, [
558
- scopedOrgId,
559
648
  messages
560
649
  ]);
561
650
  const entitled = orgReady && checkEntitlement(org, 'aiAssist');
@@ -574,10 +663,16 @@ export function AssistPanelComponent(props) {
574
663
  // jobs that spend between chat messages move it (AGL-3722).
575
664
  useEffect(()=>followAiJobsForUsageMeter(), []);
576
665
  const jobsActivity = aiJobsActivity(inFlight);
666
+ // On a site, its jobs are read on its AI jobs page (AGL-4116): the list in
667
+ // the chat was too cramped to read, and opening it scrolled the chat to
668
+ // its top.
669
+ const siteJobsHref = assistSiteAiJobsHref(pathname, orgSlug, hostId);
670
+ const router = useRouter();
577
671
  // Any surface asking to open AI jobs — an "Open AI jobs" button, the
578
- // indicator, a notification's link — opens the panel on its drawer.
579
- // The request is handed to the drawer until the panel closes, so opening
580
- // the panel again later does not re-highlight a job asked for before.
672
+ // indicator, a notification's link — goes to the site's AI jobs page, on
673
+ // the job it names. Off a site, it opens the panel on its drawer, and the
674
+ // request is handed to the drawer until the panel closes, so opening the
675
+ // panel again later does not re-highlight a job asked for before.
581
676
  const openRequest = useAiJobsOpenRequest();
582
677
  const [jobsFocus, setJobsFocus] = useState(null);
583
678
  // The request outlives the panel: it is module state, and a page in
@@ -587,6 +682,11 @@ export function AssistPanelComponent(props) {
587
682
  useEffect(()=>{
588
683
  if (openRequest.seq <= answeredSeq.current) return;
589
684
  answeredSeq.current = openRequest.seq;
685
+ if (siteJobsHref) {
686
+ setOpen(false);
687
+ router.push(openRequest.jobId ? `${siteJobsHref}/${encodeURIComponent(openRequest.jobId)}` : siteJobsHref);
688
+ return;
689
+ }
590
690
  setJobsFocus(openRequest);
591
691
  setOpen(true);
592
692
  // A new `seq` is a new request; the object is read as it stands.
@@ -620,11 +720,14 @@ export function AssistPanelComponent(props) {
620
720
  // The drafts this thread's builds made (AGL-3616): the chat sees refs and
621
721
  // labels; the addresses stay here, for the card a follow-up raises.
622
722
  const draftLinks = hostId ? assistThreadDraftLinks(messages, orgSlug) : [];
623
- const history = messages.slice(-HISTORY_TURNS_SENT).map(({ role, text })=>({
624
- role,
625
- text
723
+ // Only this site's turns ride the question (AGL-4116), and the answer
724
+ // lands in the thread it was asked in.
725
+ const history = messages.slice(-HISTORY_TURNS_SENT).map((message)=>({
726
+ role: message.role,
727
+ text: assistHistoryText(message)
626
728
  }));
627
- setMessages((prior)=>[
729
+ const askedIn = threadKey;
730
+ updateThread(askedIn, (prior)=>[
628
731
  ...prior,
629
732
  {
630
733
  role: 'user',
@@ -636,7 +739,7 @@ export function AssistPanelComponent(props) {
636
739
  }
637
740
  ]);
638
741
  const patchAnswer = (patch)=>{
639
- setMessages((prior)=>{
742
+ updateThread(askedIn, (prior)=>{
640
743
  const next = [
641
744
  ...prior
642
745
  ];
@@ -655,15 +758,18 @@ export function AssistPanelComponent(props) {
655
758
  }));
656
759
  };
657
760
  /** The build this turn proposed, started once its stream ends (AGL-3616). */ let proposedBuild = null;
761
+ /** The page change this turn asked for, started once its stream ends (AGL-4116). */ let proposedChange = null;
658
762
  try {
659
763
  var _ref;
660
764
  // The canvas outline rides along from the besigner document the open
661
- // editor holds — ids, component ids and short values, never the stored
662
- // document — and only while that editor is on the version the route
663
- // names, so the outline and the selection are the ones on screen.
765
+ // editor holds, every element with its settings and styles (AGL-4116),
766
+ // and only while that editor is on the version the route names, so the
767
+ // outline and the selection are the ones on screen.
664
768
  const editDocument = editRungHint ? assistEditDocumentOf(pathname != null ? pathname : '') : null;
665
769
  const session = editDocument ? openEditorSession() : undefined;
666
- const canvasOutline = editDocument && session && session.documentKind === editDocument.kind && session.documentId === editDocument.documentId && session.versionId === editDocument.versionId ? describeAssistEditCanvas(canvas, (_ref = session.selectedNodeId == null ? void 0 : session.selectedNodeId.call(session)) != null ? _ref : null) : null;
770
+ const canvasOutline = editDocument && session && session.documentKind === editDocument.kind && session.documentId === editDocument.documentId && session.versionId === editDocument.versionId ? describeAssistEditCanvas(canvas, (_ref = session.selectedNodeId == null ? void 0 : session.selectedNodeId.call(session)) != null ? _ref : null, {
771
+ whole: true
772
+ }) : null;
667
773
  const response = await authorizedFetch(user, '/api/assist/chat', {
668
774
  method: 'POST',
669
775
  headers: {
@@ -782,6 +888,9 @@ export function AssistPanelComponent(props) {
782
888
  // The build proposal (AGL-3616), shape-checked before it can start a job.
783
889
  const build = isAssistBuildProposal(event.build) ? event.build : null;
784
890
  proposedBuild = build;
891
+ // The page change (AGL-4116), shape-checked before it can start a job.
892
+ const pageChange = isAssistPageChangeProposal(event.pageChange) ? event.pageChange : null;
893
+ proposedChange = pageChange;
785
894
  patchAnswer((message)=>{
786
895
  var _event_exchangeId;
787
896
  return _extends({}, message, {
@@ -793,6 +902,10 @@ export function AssistPanelComponent(props) {
793
902
  build,
794
903
  buildJob: null,
795
904
  buildNotice: null
905
+ } : {}, pageChange ? {
906
+ pageChange,
907
+ pageChangeJob: null,
908
+ pageChangeNotice: null
796
909
  } : {});
797
910
  });
798
911
  if (event.quota) setQuota(event.quota);
@@ -816,6 +929,11 @@ export function AssistPanelComponent(props) {
816
929
  action: build.id
817
930
  });
818
931
  }
932
+ if (pageChange) {
933
+ trackEvent('assistant_proposal_shown', {
934
+ action: pageChange.id
935
+ });
936
+ }
819
937
  } else if (event.type === 'error') {
820
938
  var _event_error;
821
939
  failAnswer(String((_event_error = event.error) != null ? _event_error : 'The assistant stream failed.'));
@@ -825,6 +943,21 @@ export function AssistPanelComponent(props) {
825
943
  // One request, one build (AGL-3616): the job is created here, once,
826
944
  // through the job door, and plans inline; its card shows the plan.
827
945
  if (proposedBuild) {
946
+ // One plan per conversation (AGL-4116): a plan this thread proposed
947
+ // and the person has not confirmed is replaced by the new one, which
948
+ // the chat wrote from the whole request as it now stands. Its job is
949
+ // canceled, so it never builds and never warns another door.
950
+ for (const replaced of assistUnconfirmedBuilds(messages)){
951
+ updateThread(askedIn, (prior)=>prior.map((entry, i)=>i === replaced.index ? _extends({}, entry, {
952
+ buildReplaced: true
953
+ }) : entry));
954
+ void cancelAiJobRequest(user, scopedOrgId, replaced.jobId).then(({ job })=>{
955
+ if (!job) return;
956
+ updateThread(askedIn, (prior)=>prior.map((entry, i)=>i === replaced.index ? _extends({}, entry, {
957
+ buildJob: job
958
+ }) : entry));
959
+ });
960
+ }
828
961
  const decision = await startAssistBuildRequest(user, scopedOrgId, proposedBuild);
829
962
  // The plan the door ran inline has just spent (AGL-3722), so the strip
830
963
  // moves with it, to the figure the plan card quotes. Left to the jobs
@@ -843,6 +976,17 @@ export function AssistPanelComponent(props) {
843
976
  buildNotice: decision.error
844
977
  }));
845
978
  }
979
+ // A change to a page (AGL-4116): its edit job starts here, once, with
980
+ // no plan to confirm. A follow-up to a page this conversation already
981
+ // changed starts from the draft that change made.
982
+ if (proposedChange) {
983
+ const decision = await startAssistPageChangeRequest(user, scopedOrgId, proposedChange, assistPageChangeDraftVersion(messages, proposedChange.pageId));
984
+ publishMeter(decision.meter);
985
+ patchAnswer((message)=>_extends({}, message, {
986
+ pageChangeJob: decision.job,
987
+ pageChangeNotice: decision.error
988
+ }));
989
+ }
846
990
  } catch (error) {
847
991
  console.error(error);
848
992
  failAnswer('The assistant request failed — check your connection and try again.');
@@ -861,6 +1005,8 @@ export function AssistPanelComponent(props) {
861
1005
  scopedOrgId,
862
1006
  orgSlug,
863
1007
  pathname,
1008
+ threadKey,
1009
+ updateThread,
864
1010
  user
865
1011
  ]);
866
1012
  // A follow-up waiting for its draft's canvas (AGL-3616): kept per org, so a
@@ -924,7 +1070,9 @@ export function AssistPanelComponent(props) {
924
1070
  setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, {
925
1071
  proposalResolved: true
926
1072
  }) : entry));
927
- }, []);
1073
+ }, [
1074
+ setMessages
1075
+ ]);
928
1076
  /**
929
1077
  * Land a confirmed edit on the open editor (AGL-2906): through the canvas's
930
1078
  * own mutators, as one undo step, unsaved. The only request that follows is
@@ -979,18 +1127,52 @@ export function AssistPanelComponent(props) {
979
1127
  enqueueSnackbar,
980
1128
  messages,
981
1129
  scopedOrgId,
1130
+ setMessages,
982
1131
  user
983
1132
  ]);
984
1133
  /** Keep what a build card learned about its job on its own message (AGL-3616). */ const patchBuild = useCallback((index, patch)=>{
985
1134
  setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, patch) : entry));
986
- }, []);
1135
+ }, [
1136
+ setMessages
1137
+ ]);
1138
+ /** Keep what a page change card learned about its job on its own message (AGL-4116). */ const patchPageChange = useCallback((index, patch)=>{
1139
+ setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, patch) : entry));
1140
+ }, [
1141
+ setMessages
1142
+ ]);
1143
+ /** Start a page change again after it failed: a new edit job, from the same draft. */ const [retryingChange, setRetryingChange] = useState(null);
1144
+ const retryPageChange = useCallback(async (index)=>{
1145
+ var _messages_index;
1146
+ const proposal = (_messages_index = messages[index]) == null ? void 0 : _messages_index.pageChange;
1147
+ if (!proposal || !scopedOrgId) return;
1148
+ setRetryingChange(index);
1149
+ patchPageChange(index, {
1150
+ pageChangeJob: null,
1151
+ pageChangeNotice: null
1152
+ });
1153
+ const decision = await startAssistPageChangeRequest(user, scopedOrgId, proposal, assistPageChangeDraftVersion(messages.slice(0, index), proposal.pageId));
1154
+ publishMeter(decision.meter);
1155
+ patchPageChange(index, {
1156
+ pageChangeJob: decision.job,
1157
+ pageChangeNotice: decision.error
1158
+ });
1159
+ setRetryingChange(null);
1160
+ }, [
1161
+ messages,
1162
+ patchPageChange,
1163
+ publishMeter,
1164
+ scopedOrgId,
1165
+ user
1166
+ ]);
987
1167
  /** Wave an edit card away. Nothing to tell the server: nothing happened. */ const dismissEdit = useCallback((index)=>{
988
1168
  setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, {
989
1169
  editOutcome: {
990
1170
  kind: 'dismissed'
991
1171
  }
992
1172
  }) : entry));
993
- }, []);
1173
+ }, [
1174
+ setMessages
1175
+ ]);
994
1176
  const sendFeedback = useCallback(async (index, feedback)=>{
995
1177
  const message = messages[index];
996
1178
  if (!(message == null ? void 0 : message.exchangeId) || !scopedOrgId) return;
@@ -1022,6 +1204,7 @@ export function AssistPanelComponent(props) {
1022
1204
  enqueueSnackbar,
1023
1205
  messages,
1024
1206
  scopedOrgId,
1207
+ setMessages,
1025
1208
  user
1026
1209
  ]);
1027
1210
  // A released-off feature does not exist (staff preview excepted) — and
@@ -1163,7 +1346,8 @@ export function AssistPanelComponent(props) {
1163
1346
  // the jobs door where the member may not generate.
1164
1347
  insight: ai.generate ? aiInsightSurfaceForPath(pathname) : null,
1165
1348
  inFlight: jobsVisible ? inFlight : undefined,
1166
- focus: jobsFocus
1349
+ focus: jobsFocus,
1350
+ pageHref: siteJobsHref
1167
1351
  }),
1168
1352
  !messages.length && /*#__PURE__*/ _jsxs(Alert, {
1169
1353
  severity: "info",
@@ -1189,7 +1373,7 @@ export function AssistPanelComponent(props) {
1189
1373
  /*#__PURE__*/ _jsx(Stack, {
1190
1374
  spacing: 1.5,
1191
1375
  children: messages.map((message, index)=>{
1192
- var _message_buildJob, _message_buildNotice;
1376
+ var _message_pageChangeJob, _message_pageChangeNotice, _message_buildJob, _message_buildNotice;
1193
1377
  var _message_editOutcome, _message_editOutcome1, _message_editOutcome2, _message_editOutcome3;
1194
1378
  return /*#__PURE__*/ _jsxs(Box, {
1195
1379
  sx: {
@@ -1249,7 +1433,29 @@ export function AssistPanelComponent(props) {
1249
1433
  },
1250
1434
  onDismiss: ()=>dismissEdit(index)
1251
1435
  }),
1252
- message.role === 'assistant' && message.build && scopedOrgId && /*#__PURE__*/ _jsx(AssistBuildCard, {
1436
+ message.role === 'assistant' && message.pageChange && scopedOrgId && /*#__PURE__*/ _jsx(AssistPageChangeCard, {
1437
+ proposal: message.pageChange,
1438
+ job: (_message_pageChangeJob = message.pageChangeJob) != null ? _message_pageChangeJob : null,
1439
+ notice: (_message_pageChangeNotice = message.pageChangeNotice) != null ? _message_pageChangeNotice : null,
1440
+ orgId: scopedOrgId,
1441
+ orgSlug: orgSlug,
1442
+ user: user,
1443
+ busy: retryingChange === index,
1444
+ onJob: (job)=>patchPageChange(index, {
1445
+ pageChangeJob: job
1446
+ }),
1447
+ onRetry: ()=>void retryPageChange(index)
1448
+ }),
1449
+ message.role === 'assistant' && message.build && message.buildReplaced && /*#__PURE__*/ _jsx(Typography, {
1450
+ variant: "caption",
1451
+ color: "text.secondary",
1452
+ component: "div",
1453
+ sx: {
1454
+ mt: 0.5
1455
+ },
1456
+ children: ASSIST_BUILD_REPLACED_COPY
1457
+ }),
1458
+ message.role === 'assistant' && message.build && !message.buildReplaced && scopedOrgId && /*#__PURE__*/ _jsx(AssistBuildCard, {
1253
1459
  proposal: message.build,
1254
1460
  job: (_message_buildJob = message.buildJob) != null ? _message_buildJob : null,
1255
1461
  notice: (_message_buildNotice = message.buildNotice) != null ? _message_buildNotice : null,