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

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 (57) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +15 -2
  3. package/src/lib/components/ai-job-links.js +24 -3
  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-jobs-drawer.component.d.ts +7 -1
  11. package/src/lib/components/assist-jobs-drawer.component.js +20 -6
  12. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  13. package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
  14. package/src/lib/components/assist-page-change-card.component.js +166 -0
  15. package/src/lib/components/assist-page-change-card.component.js.map +1 -0
  16. package/src/lib/components/assist-panel.component.d.ts +105 -1
  17. package/src/lib/components/assist-panel.component.js +224 -29
  18. package/src/lib/components/assist-panel.component.js.map +1 -1
  19. package/src/lib/jobs/ai-job-edit-step.d.ts +62 -16
  20. package/src/lib/jobs/ai-job-edit-step.js +172 -61
  21. package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
  22. package/src/lib/jobs/ai-job-navigation-step.js +4 -2
  23. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  24. package/src/lib/model/ai-edit-job.d.ts +33 -16
  25. package/src/lib/model/ai-edit-job.js +50 -34
  26. package/src/lib/model/ai-edit-job.js.map +1 -1
  27. package/src/lib/model/ai-job-activity.d.ts +6 -11
  28. package/src/lib/model/ai-job-activity.js +8 -15
  29. package/src/lib/model/ai-job-activity.js.map +1 -1
  30. package/src/lib/model/ai-jobs.types.d.ts +6 -0
  31. package/src/lib/model/ai-jobs.types.js.map +1 -1
  32. package/src/lib/model/ai-site-inventory.d.ts +6 -0
  33. package/src/lib/model/ai-site-inventory.js.map +1 -1
  34. package/src/lib/model/assist-edit.d.ts +18 -1
  35. package/src/lib/model/assist-edit.js +7 -0
  36. package/src/lib/model/assist-edit.js.map +1 -1
  37. package/src/lib/model/assist-page-change.d.ts +86 -0
  38. package/src/lib/model/assist-page-change.js +86 -0
  39. package/src/lib/model/assist-page-change.js.map +1 -0
  40. package/src/lib/runtime/ai-eval.js +3 -1
  41. package/src/lib/runtime/ai-eval.js.map +1 -1
  42. package/src/lib/runtime/ai-node-tree.d.ts +5 -0
  43. package/src/lib/runtime/ai-node-tree.js +18 -3
  44. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  45. package/src/lib/runtime/site-inventory.d.ts +8 -1
  46. package/src/lib/runtime/site-inventory.js +18 -0
  47. package/src/lib/runtime/site-inventory.js.map +1 -1
  48. package/src/lib/server/assist-build.js +2 -0
  49. package/src/lib/server/assist-build.js.map +1 -1
  50. package/src/lib/server/assist-chat.js +51 -2
  51. package/src/lib/server/assist-chat.js.map +1 -1
  52. package/src/lib/server/assist-edit.d.ts +54 -2
  53. package/src/lib/server/assist-edit.js +219 -30
  54. package/src/lib/server/assist-edit.js.map +1 -1
  55. package/src/lib/server/assist-page-change.d.ts +62 -0
  56. package/src/lib/server/assist-page-change.js +140 -0
  57. 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, 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";
@@ -80,28 +83,69 @@ function saveFollowUp(orgId, followUp) {
80
83
  ] : []).reverse();
81
84
  return assistBuildDraftLinks(jobs, orgSlug);
82
85
  }
86
+ /**
87
+ * The builds in a thread whose plan is still waiting on the person
88
+ * (AGL-4116): queued, being planned, or planned and not yet confirmed. A new
89
+ * plan in the same thread replaces them, so a conversation that keeps
90
+ * changing a plan holds one plan, not one per message.
91
+ */ export function assistUnconfirmedBuilds(messages) {
92
+ return messages.flatMap((message, index)=>{
93
+ const job = message.build && !message.buildReplaced ? message.buildJob : null;
94
+ if (!job) return [];
95
+ const phase = aiJobPhase(job);
96
+ return phase === 'queued' || phase === 'planning' || phase === 'plan-ready' ? [
97
+ {
98
+ index,
99
+ jobId: job.id
100
+ }
101
+ ] : [];
102
+ });
103
+ }
104
+ /** 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
105
  /** 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.';
106
+ /** 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
107
  const HISTORY_TURNS_SENT = 12;
85
- const storageKey = (orgId)=>`aglyn-assist:${orgId}`;
86
- function loadThread(orgId) {
87
- if (!orgId || typeof sessionStorage === 'undefined') return [];
108
+ /**
109
+ * The thread a question belongs to: one per site, and one for the
110
+ * workspace's own pages (AGL-4116). A workspace-wide thread carried one
111
+ * site's request into a build on another site, because the history that
112
+ * rides each question is the thread's last turns.
113
+ */ export function assistThreadKey(orgId, hostId) {
114
+ return hostId ? `aglyn-assist:${orgId}:${hostId}` : `aglyn-assist:${orgId}`;
115
+ }
116
+ const NO_MESSAGES = [];
117
+ /**
118
+ * The AI jobs page of the site a console page is on (AGL-4116), or `null`
119
+ * off a site: `/{orgSlug}/hosts/{site}/ai-jobs`, the site named as the
120
+ * address names it.
121
+ */ export function assistSiteAiJobsHref(pathname, orgSlug, hostId) {
122
+ if (!pathname || !orgSlug || !hostId) return null;
123
+ const parts = pathname.split('/').filter(Boolean);
124
+ const site = parts[0] === orgSlug && parts[1] === 'hosts' ? parts[2] : undefined;
125
+ return site ? `/${orgSlug}/hosts/${site}${AI_SITE_BUILD_HREF}` : null;
126
+ }
127
+ function loadThread(key) {
128
+ if (typeof sessionStorage === 'undefined') return [];
88
129
  try {
89
- const raw = sessionStorage.getItem(storageKey(orgId));
130
+ const raw = sessionStorage.getItem(key);
90
131
  const parsed = raw ? JSON.parse(raw) : [];
91
132
  if (!Array.isArray(parsed)) return [];
92
133
  // A build whose job was still being created when the page went away is
93
134
  // not started again from a stored thread (AGL-3616): it says so instead.
135
+ // A page change likewise (AGL-4116).
94
136
  return parsed.map((message)=>message.build && !message.buildJob && !message.buildNotice ? _extends({}, message, {
95
137
  buildNotice: BUILD_NOT_STARTED
138
+ }) : message.pageChange && !message.pageChangeJob && !message.pageChangeNotice ? _extends({}, message, {
139
+ pageChangeNotice: PAGE_CHANGE_NOT_STARTED
96
140
  }) : message);
97
141
  } catch (unused) {
98
142
  return [];
99
143
  }
100
144
  }
101
- function saveThread(orgId, messages) {
102
- if (!orgId || typeof sessionStorage === 'undefined') return;
145
+ function saveThread(key, messages) {
146
+ if (typeof sessionStorage === 'undefined') return;
103
147
  try {
104
- sessionStorage.setItem(storageKey(orgId), JSON.stringify(messages));
148
+ sessionStorage.setItem(key, JSON.stringify(messages));
105
149
  } catch (unused) {
106
150
  // Quota/full storage never breaks chat.
107
151
  }
@@ -523,7 +567,25 @@ export function AssistPanelComponent(props) {
523
567
  const { enqueueSnackbar } = useSnackbar();
524
568
  const [open, setOpen] = useState(false);
525
569
  const [busy, setBusy] = useState(false);
526
- const [messages, setMessages] = useState([]);
570
+ // Every thread this panel has opened, by its key (AGL-4116): the one on
571
+ // screen is this site's, and an answer still streaming lands in the thread
572
+ // it was asked in, even after the page moved to another site.
573
+ const threadKey = scopedOrgId ? assistThreadKey(scopedOrgId, hostId) : null;
574
+ const [threads, setThreads] = useState({});
575
+ const messages = threadKey && threads[threadKey] || NO_MESSAGES;
576
+ const updateThread = useCallback((key, update)=>{
577
+ if (!key) return;
578
+ setThreads((prior)=>{
579
+ var _prior_key;
580
+ return _extends({}, prior, {
581
+ [key]: update((_prior_key = prior[key]) != null ? _prior_key : loadThread(key))
582
+ });
583
+ });
584
+ }, []);
585
+ const setMessages = useCallback((update)=>updateThread(threadKey, update), [
586
+ threadKey,
587
+ updateThread
588
+ ]);
527
589
  const [quota, setQuota] = useState(null);
528
590
  const scrollRef = useRef(null);
529
591
  // The model switch and the usage strip (AGL-2942): the pick rides the
@@ -540,22 +602,40 @@ export function AssistPanelComponent(props) {
540
602
  // the one open.
541
603
  const editorRevision = useSyncExternalStore(subscribeEditorSessions, editorSessionsRevision, editorSessionsRevision);
542
604
  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.
605
+ // A thread per site, session-persisted; moving to another site or org
606
+ // swaps threads (AGL-4116). Keyed on the SCOPED org (AGL-1934): gating only
607
+ // the render would still load — and, on the next keystroke, save — a
608
+ // thread under the fallback org's key on every mount of a page that named
609
+ // no workspace.
610
+ useEffect(()=>{
611
+ if (!threadKey) return;
612
+ setThreads((prior)=>threadKey in prior ? prior : _extends({}, prior, {
613
+ [threadKey]: loadThread(threadKey)
614
+ }));
615
+ }, [
616
+ threadKey
617
+ ]);
547
618
  useEffect(()=>{
548
- setMessages(loadThread(scopedOrgId));
549
619
  setQuota(null);
550
620
  }, [
551
621
  scopedOrgId
552
622
  ]);
623
+ // Each thread is written back when it changes, the one on screen or one an
624
+ // answer is still streaming into.
625
+ const savedThreads = useRef({});
626
+ useEffect(()=>{
627
+ for (const [key, list] of Object.entries(threads)){
628
+ if (savedThreads.current[key] === list) continue;
629
+ savedThreads.current[key] = list;
630
+ saveThread(key, list);
631
+ }
632
+ }, [
633
+ threads
634
+ ]);
553
635
  useEffect(()=>{
554
- saveThread(scopedOrgId, messages);
555
636
  const node = scrollRef.current;
556
637
  if (node) node.scrollTop = node.scrollHeight;
557
638
  }, [
558
- scopedOrgId,
559
639
  messages
560
640
  ]);
561
641
  const entitled = orgReady && checkEntitlement(org, 'aiAssist');
@@ -574,10 +654,16 @@ export function AssistPanelComponent(props) {
574
654
  // jobs that spend between chat messages move it (AGL-3722).
575
655
  useEffect(()=>followAiJobsForUsageMeter(), []);
576
656
  const jobsActivity = aiJobsActivity(inFlight);
657
+ // On a site, its jobs are read on its AI jobs page (AGL-4116): the list in
658
+ // the chat was too cramped to read, and opening it scrolled the chat to
659
+ // its top.
660
+ const siteJobsHref = assistSiteAiJobsHref(pathname, orgSlug, hostId);
661
+ const router = useRouter();
577
662
  // 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.
663
+ // indicator, a notification's link — goes to the site's AI jobs page, on
664
+ // the job it names. Off a site, it opens the panel on its drawer, and the
665
+ // request is handed to the drawer until the panel closes, so opening the
666
+ // panel again later does not re-highlight a job asked for before.
581
667
  const openRequest = useAiJobsOpenRequest();
582
668
  const [jobsFocus, setJobsFocus] = useState(null);
583
669
  // The request outlives the panel: it is module state, and a page in
@@ -587,6 +673,11 @@ export function AssistPanelComponent(props) {
587
673
  useEffect(()=>{
588
674
  if (openRequest.seq <= answeredSeq.current) return;
589
675
  answeredSeq.current = openRequest.seq;
676
+ if (siteJobsHref) {
677
+ setOpen(false);
678
+ router.push(openRequest.jobId ? `${siteJobsHref}/${encodeURIComponent(openRequest.jobId)}` : siteJobsHref);
679
+ return;
680
+ }
590
681
  setJobsFocus(openRequest);
591
682
  setOpen(true);
592
683
  // A new `seq` is a new request; the object is read as it stands.
@@ -620,11 +711,14 @@ export function AssistPanelComponent(props) {
620
711
  // The drafts this thread's builds made (AGL-3616): the chat sees refs and
621
712
  // labels; the addresses stay here, for the card a follow-up raises.
622
713
  const draftLinks = hostId ? assistThreadDraftLinks(messages, orgSlug) : [];
714
+ // Only this site's turns ride the question (AGL-4116), and the answer
715
+ // lands in the thread it was asked in.
623
716
  const history = messages.slice(-HISTORY_TURNS_SENT).map(({ role, text })=>({
624
717
  role,
625
718
  text
626
719
  }));
627
- setMessages((prior)=>[
720
+ const askedIn = threadKey;
721
+ updateThread(askedIn, (prior)=>[
628
722
  ...prior,
629
723
  {
630
724
  role: 'user',
@@ -636,7 +730,7 @@ export function AssistPanelComponent(props) {
636
730
  }
637
731
  ]);
638
732
  const patchAnswer = (patch)=>{
639
- setMessages((prior)=>{
733
+ updateThread(askedIn, (prior)=>{
640
734
  const next = [
641
735
  ...prior
642
736
  ];
@@ -655,6 +749,7 @@ export function AssistPanelComponent(props) {
655
749
  }));
656
750
  };
657
751
  /** The build this turn proposed, started once its stream ends (AGL-3616). */ let proposedBuild = null;
752
+ /** The page change this turn asked for, started once its stream ends (AGL-4116). */ let proposedChange = null;
658
753
  try {
659
754
  var _ref;
660
755
  // The canvas outline rides along from the besigner document the open
@@ -782,6 +877,9 @@ export function AssistPanelComponent(props) {
782
877
  // The build proposal (AGL-3616), shape-checked before it can start a job.
783
878
  const build = isAssistBuildProposal(event.build) ? event.build : null;
784
879
  proposedBuild = build;
880
+ // The page change (AGL-4116), shape-checked before it can start a job.
881
+ const pageChange = isAssistPageChangeProposal(event.pageChange) ? event.pageChange : null;
882
+ proposedChange = pageChange;
785
883
  patchAnswer((message)=>{
786
884
  var _event_exchangeId;
787
885
  return _extends({}, message, {
@@ -793,6 +891,10 @@ export function AssistPanelComponent(props) {
793
891
  build,
794
892
  buildJob: null,
795
893
  buildNotice: null
894
+ } : {}, pageChange ? {
895
+ pageChange,
896
+ pageChangeJob: null,
897
+ pageChangeNotice: null
796
898
  } : {});
797
899
  });
798
900
  if (event.quota) setQuota(event.quota);
@@ -816,6 +918,11 @@ export function AssistPanelComponent(props) {
816
918
  action: build.id
817
919
  });
818
920
  }
921
+ if (pageChange) {
922
+ trackEvent('assistant_proposal_shown', {
923
+ action: pageChange.id
924
+ });
925
+ }
819
926
  } else if (event.type === 'error') {
820
927
  var _event_error;
821
928
  failAnswer(String((_event_error = event.error) != null ? _event_error : 'The assistant stream failed.'));
@@ -825,6 +932,21 @@ export function AssistPanelComponent(props) {
825
932
  // One request, one build (AGL-3616): the job is created here, once,
826
933
  // through the job door, and plans inline; its card shows the plan.
827
934
  if (proposedBuild) {
935
+ // One plan per conversation (AGL-4116): a plan this thread proposed
936
+ // and the person has not confirmed is replaced by the new one, which
937
+ // the chat wrote from the whole request as it now stands. Its job is
938
+ // canceled, so it never builds and never warns another door.
939
+ for (const replaced of assistUnconfirmedBuilds(messages)){
940
+ updateThread(askedIn, (prior)=>prior.map((entry, i)=>i === replaced.index ? _extends({}, entry, {
941
+ buildReplaced: true
942
+ }) : entry));
943
+ void cancelAiJobRequest(user, scopedOrgId, replaced.jobId).then(({ job })=>{
944
+ if (!job) return;
945
+ updateThread(askedIn, (prior)=>prior.map((entry, i)=>i === replaced.index ? _extends({}, entry, {
946
+ buildJob: job
947
+ }) : entry));
948
+ });
949
+ }
828
950
  const decision = await startAssistBuildRequest(user, scopedOrgId, proposedBuild);
829
951
  // The plan the door ran inline has just spent (AGL-3722), so the strip
830
952
  // moves with it, to the figure the plan card quotes. Left to the jobs
@@ -843,6 +965,17 @@ export function AssistPanelComponent(props) {
843
965
  buildNotice: decision.error
844
966
  }));
845
967
  }
968
+ // A change to a page (AGL-4116): its edit job starts here, once, with
969
+ // no plan to confirm. A follow-up to a page this conversation already
970
+ // changed starts from the draft that change made.
971
+ if (proposedChange) {
972
+ const decision = await startAssistPageChangeRequest(user, scopedOrgId, proposedChange, assistPageChangeDraftVersion(messages, proposedChange.pageId));
973
+ publishMeter(decision.meter);
974
+ patchAnswer((message)=>_extends({}, message, {
975
+ pageChangeJob: decision.job,
976
+ pageChangeNotice: decision.error
977
+ }));
978
+ }
846
979
  } catch (error) {
847
980
  console.error(error);
848
981
  failAnswer('The assistant request failed — check your connection and try again.');
@@ -861,6 +994,8 @@ export function AssistPanelComponent(props) {
861
994
  scopedOrgId,
862
995
  orgSlug,
863
996
  pathname,
997
+ threadKey,
998
+ updateThread,
864
999
  user
865
1000
  ]);
866
1001
  // A follow-up waiting for its draft's canvas (AGL-3616): kept per org, so a
@@ -924,7 +1059,9 @@ export function AssistPanelComponent(props) {
924
1059
  setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, {
925
1060
  proposalResolved: true
926
1061
  }) : entry));
927
- }, []);
1062
+ }, [
1063
+ setMessages
1064
+ ]);
928
1065
  /**
929
1066
  * Land a confirmed edit on the open editor (AGL-2906): through the canvas's
930
1067
  * own mutators, as one undo step, unsaved. The only request that follows is
@@ -979,18 +1116,52 @@ export function AssistPanelComponent(props) {
979
1116
  enqueueSnackbar,
980
1117
  messages,
981
1118
  scopedOrgId,
1119
+ setMessages,
982
1120
  user
983
1121
  ]);
984
1122
  /** Keep what a build card learned about its job on its own message (AGL-3616). */ const patchBuild = useCallback((index, patch)=>{
985
1123
  setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, patch) : entry));
986
- }, []);
1124
+ }, [
1125
+ setMessages
1126
+ ]);
1127
+ /** Keep what a page change card learned about its job on its own message (AGL-4116). */ const patchPageChange = useCallback((index, patch)=>{
1128
+ setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, patch) : entry));
1129
+ }, [
1130
+ setMessages
1131
+ ]);
1132
+ /** Start a page change again after it failed: a new edit job, from the same draft. */ const [retryingChange, setRetryingChange] = useState(null);
1133
+ const retryPageChange = useCallback(async (index)=>{
1134
+ var _messages_index;
1135
+ const proposal = (_messages_index = messages[index]) == null ? void 0 : _messages_index.pageChange;
1136
+ if (!proposal || !scopedOrgId) return;
1137
+ setRetryingChange(index);
1138
+ patchPageChange(index, {
1139
+ pageChangeJob: null,
1140
+ pageChangeNotice: null
1141
+ });
1142
+ const decision = await startAssistPageChangeRequest(user, scopedOrgId, proposal, assistPageChangeDraftVersion(messages.slice(0, index), proposal.pageId));
1143
+ publishMeter(decision.meter);
1144
+ patchPageChange(index, {
1145
+ pageChangeJob: decision.job,
1146
+ pageChangeNotice: decision.error
1147
+ });
1148
+ setRetryingChange(null);
1149
+ }, [
1150
+ messages,
1151
+ patchPageChange,
1152
+ publishMeter,
1153
+ scopedOrgId,
1154
+ user
1155
+ ]);
987
1156
  /** Wave an edit card away. Nothing to tell the server: nothing happened. */ const dismissEdit = useCallback((index)=>{
988
1157
  setMessages((prior)=>prior.map((entry, i)=>i === index ? _extends({}, entry, {
989
1158
  editOutcome: {
990
1159
  kind: 'dismissed'
991
1160
  }
992
1161
  }) : entry));
993
- }, []);
1162
+ }, [
1163
+ setMessages
1164
+ ]);
994
1165
  const sendFeedback = useCallback(async (index, feedback)=>{
995
1166
  const message = messages[index];
996
1167
  if (!(message == null ? void 0 : message.exchangeId) || !scopedOrgId) return;
@@ -1022,6 +1193,7 @@ export function AssistPanelComponent(props) {
1022
1193
  enqueueSnackbar,
1023
1194
  messages,
1024
1195
  scopedOrgId,
1196
+ setMessages,
1025
1197
  user
1026
1198
  ]);
1027
1199
  // A released-off feature does not exist (staff preview excepted) — and
@@ -1163,7 +1335,8 @@ export function AssistPanelComponent(props) {
1163
1335
  // the jobs door where the member may not generate.
1164
1336
  insight: ai.generate ? aiInsightSurfaceForPath(pathname) : null,
1165
1337
  inFlight: jobsVisible ? inFlight : undefined,
1166
- focus: jobsFocus
1338
+ focus: jobsFocus,
1339
+ pageHref: siteJobsHref
1167
1340
  }),
1168
1341
  !messages.length && /*#__PURE__*/ _jsxs(Alert, {
1169
1342
  severity: "info",
@@ -1189,7 +1362,7 @@ export function AssistPanelComponent(props) {
1189
1362
  /*#__PURE__*/ _jsx(Stack, {
1190
1363
  spacing: 1.5,
1191
1364
  children: messages.map((message, index)=>{
1192
- var _message_buildJob, _message_buildNotice;
1365
+ var _message_pageChangeJob, _message_pageChangeNotice, _message_buildJob, _message_buildNotice;
1193
1366
  var _message_editOutcome, _message_editOutcome1, _message_editOutcome2, _message_editOutcome3;
1194
1367
  return /*#__PURE__*/ _jsxs(Box, {
1195
1368
  sx: {
@@ -1249,7 +1422,29 @@ export function AssistPanelComponent(props) {
1249
1422
  },
1250
1423
  onDismiss: ()=>dismissEdit(index)
1251
1424
  }),
1252
- message.role === 'assistant' && message.build && scopedOrgId && /*#__PURE__*/ _jsx(AssistBuildCard, {
1425
+ message.role === 'assistant' && message.pageChange && scopedOrgId && /*#__PURE__*/ _jsx(AssistPageChangeCard, {
1426
+ proposal: message.pageChange,
1427
+ job: (_message_pageChangeJob = message.pageChangeJob) != null ? _message_pageChangeJob : null,
1428
+ notice: (_message_pageChangeNotice = message.pageChangeNotice) != null ? _message_pageChangeNotice : null,
1429
+ orgId: scopedOrgId,
1430
+ orgSlug: orgSlug,
1431
+ user: user,
1432
+ busy: retryingChange === index,
1433
+ onJob: (job)=>patchPageChange(index, {
1434
+ pageChangeJob: job
1435
+ }),
1436
+ onRetry: ()=>void retryPageChange(index)
1437
+ }),
1438
+ message.role === 'assistant' && message.build && message.buildReplaced && /*#__PURE__*/ _jsx(Typography, {
1439
+ variant: "caption",
1440
+ color: "text.secondary",
1441
+ component: "div",
1442
+ sx: {
1443
+ mt: 0.5
1444
+ },
1445
+ children: ASSIST_BUILD_REPLACED_COPY
1446
+ }),
1447
+ message.role === 'assistant' && message.build && !message.buildReplaced && scopedOrgId && /*#__PURE__*/ _jsx(AssistBuildCard, {
1253
1448
  proposal: message.build,
1254
1449
  job: (_message_buildJob = message.buildJob) != null ? _message_buildJob : null,
1255
1450
  notice: (_message_buildNotice = message.buildNotice) != null ? _message_buildNotice : null,