@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.227

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 (156) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/plan-entitlements.d.ts +3 -3
  105. package/src/lib/plan-entitlements.js +3 -3
  106. package/src/lib/plan-entitlements.js.map +1 -1
  107. package/src/lib/plugin.js +28 -0
  108. package/src/lib/plugin.js.map +1 -1
  109. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  110. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  111. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  112. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  113. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  114. package/src/lib/runtime/ai-gate.d.ts +3 -2
  115. package/src/lib/runtime/ai-gate.js +3 -2
  116. package/src/lib/runtime/ai-gate.js.map +1 -1
  117. package/src/lib/runtime/site-inventory.d.ts +16 -1
  118. package/src/lib/runtime/site-inventory.js +43 -3
  119. package/src/lib/runtime/site-inventory.js.map +1 -1
  120. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  121. package/src/lib/server/ai-admin-credits.js +213 -0
  122. package/src/lib/server/ai-admin-credits.js.map +1 -0
  123. package/src/lib/server/ai-admin-org.js +12 -5
  124. package/src/lib/server/ai-admin-org.js.map +1 -1
  125. package/src/lib/server/ai-admin-signals.js +3 -1
  126. package/src/lib/server/ai-admin-signals.js.map +1 -1
  127. package/src/lib/server/ai-admin-user.js +12 -1
  128. package/src/lib/server/ai-admin-user.js.map +1 -1
  129. package/src/lib/server/ai-allotments.js +3 -2
  130. package/src/lib/server/ai-allotments.js.map +1 -1
  131. package/src/lib/server/ai-jobs-route.js +20 -2
  132. package/src/lib/server/ai-jobs-route.js.map +1 -1
  133. package/src/lib/server/assist-chat.js +3 -2
  134. package/src/lib/server/assist-chat.js.map +1 -1
  135. package/src/lib/server/billing-credits.js +4 -3
  136. package/src/lib/server/billing-credits.js.map +1 -1
  137. package/src/lib/server/billing-overage.js +3 -2
  138. package/src/lib/server/billing-overage.js.map +1 -1
  139. package/src/lib/server.js +11 -0
  140. package/src/lib/server.js.map +1 -1
  141. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  142. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  143. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  144. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  145. package/src/lib/usage/assist-credit-returns.js +79 -0
  146. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  147. package/src/lib/usage/assist-free-taste.js +6 -3
  148. package/src/lib/usage/assist-free-taste.js.map +1 -1
  149. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  150. package/src/lib/usage/assist-job-refund.js +109 -0
  151. package/src/lib/usage/assist-job-refund.js.map +1 -0
  152. package/src/lib/usage/assist-usage.js +5 -2
  153. package/src/lib/usage/assist-usage.js.map +1 -1
  154. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  155. package/src/lib/usage/staff-org-ai.js +19 -2
  156. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -16,18 +16,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  * limitations under the License.
17
17
  */ import { checkEntitlement } from "@aglyn/aglyn";
18
18
  import { trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
19
- import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
20
19
  import { formatBytes } from "@aglyn/aglyn/app-utils/measure-node-map";
21
- import { pluginRecordHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
22
20
  import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
23
21
  import { mdiChevronDown, mdiChevronUp } from "@aglyn/shared-data-mdi";
24
22
  import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
25
23
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
26
24
  import { Box, Button, Chip, CircularProgress, Collapse, IconButton, Stack, Typography } from "@mui/material";
27
25
  import { useCallback, useEffect, useRef, useState } from "react";
26
+ import { AI_JOB_PHASE_LABELS, aiJobActivityCounts, aiJobPhase, aiJobsHeaderChips } from "../model/ai-job-activity.js";
27
+ import { cancelAiJobRequest, resumeAiJobRequest } from "./ai-job-requests.js";
28
+ import { publishAiJob } from "./ai-jobs-store.js";
28
29
  import { AiPageBriefDialog } from "./ai-page-brief-dialog.component.js";
29
30
  import { AiInsightDialog } from "./ai-insight-dialog.component.js";
30
31
  import { AiJobPlan } from "./ai-job-plan.component.js";
32
+ import { aiJobOutputHref, aiJobPrimaryLink } from "./ai-job-links.js";
33
+ import { readEventFrames } from "./ai-job-events.js";
34
+ // Where an output opens, and the stream reader, live beside the dialogs that
35
+ // share them (AGL-3593); this module has always exported them.
36
+ export { aiJobOutputHref, aiJobPrimaryLink, readEventFrames };
31
37
  /**
32
38
  * The AI jobs drawer inside the Assist panel (AGL-2904): the workspace's
33
39
  * running and recent generation jobs, each step's progress, an "open
@@ -40,97 +46,40 @@ import { AiJobPlan } from "./ai-job-plan.component.js";
40
46
  * panel opens on every console page, and a list nobody asked for is a read
41
47
  * on every page view.
42
48
  *
49
+ * It opens EXPANDED while any of the workspace's jobs is moving or waiting
50
+ * for the person (AGL-3593) — the shared list the top-bar indicator reads
51
+ * says so — because a plan waiting behind a collapsed row is a site that
52
+ * never gets built. An "Open AI jobs" anywhere in the console expands it,
53
+ * scrolls to the job it names and highlights it for a moment.
54
+ *
43
55
  * Hidden — not disabled — unless the flag shows it AND the plan carries
44
56
  * `aiGenerative`: a released-off feature does not exist, and a plan without
45
57
  * it is sold the add-on on the Billing page, not here.
46
58
  */ /** Jobs the drawer lists; the route caps higher, the drawer is a glance. */ const LIST_LIMIT = 10;
47
59
  /** How much of a brief a row shows. */ const BRIEF_PREVIEW_CHARS = 90;
48
- const STATUS_LABEL = {
49
- queued: 'Queued',
50
- running: 'Running',
51
- needs_input: 'Needs attention',
52
- needs_review: 'Needs review',
53
- done: 'Done',
54
- failed: 'Failed',
55
- canceled: 'Canceled'
56
- };
57
- const STATUS_COLOR = {
60
+ /**
61
+ * A row's chip says where the job stands in the words every AI surface uses
62
+ * (AGL-3593): a plan waiting to be confirmed is "Plan ready", never "Running"
63
+ * and never the machine's `needs_review`.
64
+ */ const PHASE_COLOR = {
58
65
  queued: 'default',
59
- running: 'primary',
60
- needs_input: 'warning',
61
- needs_review: 'warning',
66
+ planning: 'primary',
67
+ 'plan-ready': 'warning',
68
+ building: 'primary',
69
+ attention: 'warning',
62
70
  done: 'success',
63
71
  failed: 'error',
64
72
  canceled: 'default'
65
73
  };
66
- /** The besigner segment each versioned resource lives under. */ const BESIGNER_SEGMENT = {
67
- screen: 'screens',
68
- reusableComponent: 'components',
69
- layout: 'layouts',
70
- template: 'templates',
71
- // An email design is a screen: it opens in the screen besigner, as the
72
- // Emails page's own Edit design does.
73
- emailScreen: 'screens'
74
+ /** The header's count chips, colored as the rows they count. */ const STATE_COLOR = {
75
+ 'needs-you': 'warning',
76
+ attention: 'warning',
77
+ running: 'primary'
74
78
  };
75
- /**
76
- * Where "open draft" goes, or `null` when the output has no page of its
77
- * own — a `text` output carries its copy on the job and is shown inline.
78
- * A versioned resource opens in the besigner on the version the job wrote;
79
- * one the console lists without a detail page opens its list; a `theme`
80
- * proposal opens the site's Theme section, where it is put in the editor.
81
- *
82
- * A console URL names a site by its SUBDOMAIN: the `[host]` segment resolves
83
- * through the member's host projection by subdomain, never by document id.
84
- * So an output that carries no `hostSubdomain` gets no link rather than one
85
- * that opens no site.
86
- */ export function aiJobOutputHref(output, orgSlug) {
87
- const host = output.hostSubdomain;
88
- if (!orgSlug || !host) return null;
89
- const segment = BESIGNER_SEGMENT[output.resource];
90
- if (segment) {
91
- const base = `/${orgSlug}/hosts/${host}/${segment}/${output.id}`;
92
- return output.versionId ? `${base}/versions/${output.versionId}/besigner` : base;
93
- }
94
- if (output.resource === 'product') {
95
- // The catalog a drafted product lands in: the commerce plugin's address
96
- // for it (AGL-3080), or no link where commerce is not loaded.
97
- return pluginRecordListHref('product', {
98
- orgSlug,
99
- host
100
- });
101
- }
102
- if (output.resource === 'workflow') {
103
- // A drafted automation is an action (AGL-2919), listed switched off on
104
- // the workflows plugin's list of actions, or no link where it is not loaded.
105
- return pluginRecordListHref('action', {
106
- orgSlug,
107
- host
108
- });
109
- }
110
- if (output.resource === 'theme') {
111
- return buildRoute(Route.HOST_SETUP_THEME, {
112
- orgSlug,
113
- host
114
- });
115
- }
116
- if (output.resource === 'form') {
117
- // A new form has no version for the besigner to open: its own page — the
118
- // forms plugin's address for it — mints the first one, and holds the
119
- // routing and consent it declares.
120
- return pluginRecordHref('form', {
121
- orgSlug,
122
- host
123
- }, output.id);
124
- }
125
- if (output.resource === 'campaign') {
126
- // A campaign's page is the marketing plugin's, under the site: its
127
- // address for the campaign (AGL-3080), or no link where it is not loaded.
128
- return pluginRecordHref('campaign', {
129
- orgSlug,
130
- host
131
- }, output.id);
132
- }
133
- return null;
79
+ /** How long a job AI jobs was opened on stays highlighted. */ const HIGHLIGHT_MS = 2500;
80
+ /** What a job cost the person: what it spent, less what we refunded. */ function netCredits(job) {
81
+ var _job_creditsSpent, _job_refundedCredits;
82
+ return Math.max(0, ((_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0) - ((_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0));
134
83
  }
135
84
  /**
136
85
  * The navigation entry a page job proposes for the page it built (AGL-2907),
@@ -144,37 +93,17 @@ const STATUS_COLOR = {
144
93
  const label = navigation['label'];
145
94
  return typeof label === 'string' && label.trim() ? label.trim() : null;
146
95
  }
147
- /**
148
- * `data:` frames out of an SSE body, one parsed event per frame. Shared with
149
- * every panel that watches a job through the events route.
150
- */ export async function readEventFrames(body, onEvent) {
151
- const reader = body.getReader();
152
- const decoder = new TextDecoder();
153
- let buffer = '';
154
- for(;;){
155
- var _frames_pop;
156
- const { done, value } = await reader.read();
157
- if (done) break;
158
- buffer += decoder.decode(value, {
159
- stream: true
160
- });
161
- const frames = buffer.split('\n\n');
162
- buffer = (_frames_pop = frames.pop()) != null ? _frames_pop : '';
163
- for (const frame of frames){
164
- const line = frame.split('\n').find((part)=>part.startsWith('data: '));
165
- if (!line) continue;
166
- try {
167
- onEvent(JSON.parse(line.slice('data: '.length)));
168
- } catch (unused) {
169
- // A torn frame is dropped; the next state event carries the whole job.
170
- }
171
- }
172
- }
173
- }
174
- export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible, isStaff = false, hostId, insight }) {
175
- var _ref, _ref1, _ref2, _ref3;
96
+ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible, isStaff = false, hostId, insight, inFlight, focus }) {
97
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6;
176
98
  const entitled = orgReady && checkEntitlement(org, 'aiGenerative');
177
99
  const [expanded, setExpanded] = useState(false);
100
+ /**
101
+ * The person opened or closed the list themselves: from then on the
102
+ * expanded-by-default rule leaves their choice alone until the workspace
103
+ * changes.
104
+ */ const toggledRef = useRef(false);
105
+ /** The job AI jobs was opened on, highlighted for a moment once it is listed. */ const [highlight, setHighlight] = useState(null);
106
+ const listRef = useRef(null);
178
107
  const [jobs, setJobs] = useState([]);
179
108
  const [loading, setLoading] = useState(false);
180
109
  const [notice, setNotice] = useState(null);
@@ -186,6 +115,8 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
186
115
  /** Job ids whose terminal event has been tracked, so a re-read does not count twice. */ const trackedRef = useRef(new Set());
187
116
  /** The job whose stream is open, so a re-render does not open a second. */ const watchingRef = useRef(null);
188
117
  const patchJob = useCallback((next)=>{
118
+ // Every counting surface moves with the row (AGL-3593).
119
+ publishAiJob(next);
189
120
  setJobs((prior)=>{
190
121
  const index = prior.findIndex((job)=>job.id === next.id);
191
122
  if (index === -1) return [
@@ -229,6 +160,8 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
229
160
  // watch finish, so its outcome is not this session's to count.
230
161
  for (const job of payload.jobs){
231
162
  if (AI_JOB_TERMINAL_STATUSES.includes(job.status)) trackedRef.current.add(job.id);
163
+ // A job the indicator still counts that has settled since leaves it.
164
+ publishAiJob(job);
232
165
  }
233
166
  setJobs(payload.jobs);
234
167
  } catch (unused) {
@@ -251,15 +184,60 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
251
184
  useEffect(()=>{
252
185
  setJobs([]);
253
186
  setExpanded(false);
187
+ toggledRef.current = false;
254
188
  watchingRef.current = null;
255
189
  }, [
256
190
  orgId
257
191
  ]);
192
+ // Expanded by default while anything is moving or waiting (AGL-3593): a
193
+ // plan waiting to be confirmed behind a collapsed row is a site that never
194
+ // gets built. Otherwise collapsed, which reads nothing until asked.
195
+ const anyInFlight = ((_ref = inFlight == null ? void 0 : inFlight.length) != null ? _ref : 0) > 0;
196
+ useEffect(()=>{
197
+ if (anyInFlight && !toggledRef.current) setExpanded(true);
198
+ }, [
199
+ anyInFlight
200
+ ]);
201
+ // A request to open AI jobs (AGL-3593): expand, read the list again so a
202
+ // job created a moment ago is in it, and highlight the job named.
203
+ const focusSeq = (_ref1 = focus == null ? void 0 : focus.seq) != null ? _ref1 : 0;
204
+ const focusJobId = (_ref2 = focus == null ? void 0 : focus.jobId) != null ? _ref2 : null;
205
+ const expandedRef = useRef(expanded);
206
+ expandedRef.current = expanded;
207
+ useEffect(()=>{
208
+ if (!focusSeq) return;
209
+ toggledRef.current = true;
210
+ setHighlight(focusJobId);
211
+ // An open list does not re-run the expand effect's read, so it reads here.
212
+ if (expandedRef.current) void load();
213
+ else setExpanded(true);
214
+ // Only a new request runs this; `load` and the job are read as they stand.
215
+ // eslint-disable-next-line react-hooks/exhaustive-deps
216
+ }, [
217
+ focusSeq
218
+ ]);
219
+ // Scroll to the highlighted job once it is listed, then let it fade.
220
+ const highlightListed = highlight !== null && jobs.some((job)=>job.id === highlight);
221
+ useEffect(()=>{
222
+ var _ref;
223
+ var _listRef_current, _row_scrollIntoView;
224
+ if (!highlight || !highlightListed) return undefined;
225
+ const row = Array.from((_ref = (_listRef_current = listRef.current) == null ? void 0 : _listRef_current.querySelectorAll('[data-ai-job-id]')) != null ? _ref : []).find((element)=>element.dataset['aiJobId'] === highlight);
226
+ row == null ? void 0 : (_row_scrollIntoView = row.scrollIntoView) == null ? void 0 : _row_scrollIntoView.call(row, {
227
+ block: 'nearest',
228
+ behavior: 'smooth'
229
+ });
230
+ const timer = setTimeout(()=>setHighlight(null), HIGHLIGHT_MS);
231
+ return ()=>clearTimeout(timer);
232
+ }, [
233
+ highlight,
234
+ highlightListed
235
+ ]);
258
236
  // Watch the most recent job that is still moving. One stream at a time:
259
237
  // the route re-reads a document every two seconds per open stream, and a
260
238
  // drawer with three running jobs does not need three of them.
261
239
  const watched = jobs.find((job)=>!AI_JOB_TERMINAL_STATUSES.includes(job.status));
262
- const watchedId = (_ref = watched == null ? void 0 : watched.id) != null ? _ref : null;
240
+ const watchedId = (_ref3 = watched == null ? void 0 : watched.id) != null ? _ref3 : null;
263
241
  useEffect(()=>{
264
242
  if (!expanded || !orgId || !watchedId || watchingRef.current === watchedId) return;
265
243
  watchingRef.current = watchedId;
@@ -303,28 +281,15 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
303
281
  user,
304
282
  patchJob
305
283
  ]);
284
+ // The same door the dialog that started the job cancels through (AGL-3593).
306
285
  const cancel = useCallback(async (jobId)=>{
307
286
  if (!orgId) return;
308
- try {
309
- const response = await authorizedFetch(user, `/api/ai/jobs/${encodeURIComponent(jobId)}/cancel`, {
310
- method: 'POST',
311
- headers: {
312
- 'Content-Type': 'application/json'
313
- },
314
- body: JSON.stringify({
315
- orgId
316
- })
317
- });
318
- const payload = await response.json().catch(()=>null);
319
- if (!response.ok) {
320
- var _ref;
321
- setNotice(String((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The job could not be canceled — try again.'));
322
- return;
323
- }
324
- if (payload == null ? void 0 : payload.job) patchJob(payload.job);
325
- } catch (unused) {
326
- setNotice('The job could not be canceled — try again.');
287
+ const { job, error } = await cancelAiJobRequest(user, orgId, jobId);
288
+ if (error) {
289
+ setNotice(error);
290
+ return;
327
291
  }
292
+ if (job) patchJob(job);
328
293
  }, [
329
294
  orgId,
330
295
  user,
@@ -338,35 +303,20 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
338
303
  if (!orgId) return;
339
304
  setResuming(job.id);
340
305
  setNotice(null);
341
- try {
342
- const response = await authorizedFetch(user, `/api/ai/jobs/${encodeURIComponent(job.id)}/resume`, {
343
- method: 'POST',
344
- headers: {
345
- 'Content-Type': 'application/json'
346
- },
347
- body: JSON.stringify({
348
- orgId,
349
- hostId: job.hostId
350
- })
351
- });
352
- const payload = await response.json().catch(()=>null);
353
- if (payload == null ? void 0 : payload.job) patchJob(payload.job);
354
- if (!response.ok) {
355
- var _ref;
356
- setNotice(String((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The job could not be resumed — try again.'));
357
- }
358
- } catch (unused) {
359
- setNotice('The job could not be resumed — try again.');
360
- } finally{
361
- setResuming(null);
362
- }
306
+ // The same door the dialog that started the job confirms through (AGL-3593).
307
+ const { job: next, error } = await resumeAiJobRequest(user, orgId, job);
308
+ if (next) patchJob(next);
309
+ if (error) setNotice(error);
310
+ setResuming(null);
363
311
  }, [
364
312
  orgId,
365
313
  user,
366
314
  patchJob
367
315
  ]);
368
316
  if (!visible || !orgId || !entitled) return null;
369
- const active = jobs.filter((job)=>!AI_JOB_TERMINAL_STATUSES.includes(job.status)).length;
317
+ // Counted by state, never as one "running" figure (AGL-3593): a plan waiting
318
+ // to be confirmed needs the person, and a stopped job is not moving.
319
+ const chips = aiJobsHeaderChips(aiJobActivityCounts(inFlight != null ? inFlight : jobs));
370
320
  return /*#__PURE__*/ _jsxs(Box, {
371
321
  sx: {
372
322
  mb: 2,
@@ -389,14 +339,14 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
389
339
  },
390
340
  children: [
391
341
  "AI jobs",
392
- active > 0 && /*#__PURE__*/ _jsx(Chip, {
393
- size: "small",
394
- color: "primary",
395
- label: `${active} running`,
396
- sx: {
397
- ml: 1
398
- }
399
- })
342
+ chips.map((chip)=>/*#__PURE__*/ _jsx(Chip, {
343
+ size: "small",
344
+ color: STATE_COLOR[chip.state],
345
+ label: chip.label,
346
+ sx: {
347
+ ml: 1
348
+ }
349
+ }, chip.state))
400
350
  ]
401
351
  }),
402
352
  insight && /*#__PURE__*/ _jsx(Button, {
@@ -413,7 +363,10 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
413
363
  size: "small",
414
364
  "aria-label": expanded ? 'Hide AI jobs' : 'Show AI jobs',
415
365
  "aria-expanded": expanded,
416
- onClick: ()=>setExpanded((prior)=>!prior),
366
+ onClick: ()=>{
367
+ toggledRef.current = true;
368
+ setExpanded((prior)=>!prior);
369
+ },
417
370
  children: /*#__PURE__*/ _jsx(MdiIcon, {
418
371
  path: expanded ? mdiChevronUp.path : mdiChevronDown.path,
419
372
  sx: {
@@ -453,6 +406,7 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
453
406
  children: "No AI jobs yet."
454
407
  }),
455
408
  /*#__PURE__*/ _jsx(Stack, {
409
+ ref: listRef,
456
410
  spacing: 1,
457
411
  sx: {
458
412
  mt: 1
@@ -463,11 +417,24 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
463
417
  const stepsDone = job.steps.filter((step)=>step.status === 'done').length;
464
418
  const current = job.steps.find((step)=>step.status === 'running');
465
419
  const terminal = AI_JOB_TERMINAL_STATUSES.includes(job.status);
420
+ const phase = aiJobPhase(job);
421
+ const primary = aiJobPrimaryLink(job, orgSlug);
422
+ const highlighted = highlight === job.id;
466
423
  return /*#__PURE__*/ _jsxs(Box, {
467
424
  role: "listitem",
425
+ "data-ai-job-id": job.id,
426
+ "aria-current": highlighted ? 'true' : undefined,
468
427
  sx: {
469
428
  borderRadius: 1,
470
- bgcolor: 'action.hover',
429
+ bgcolor: highlighted ? 'action.selected' : 'action.hover',
430
+ outline: 2,
431
+ outlineColor: highlighted ? 'primary.main' : 'transparent',
432
+ transition: (theme)=>theme.transitions.create([
433
+ 'background-color',
434
+ 'outline-color'
435
+ ], {
436
+ duration: theme.transitions.duration.standard
437
+ }),
471
438
  px: 1.5,
472
439
  py: 1
473
440
  },
@@ -486,8 +453,8 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
486
453
  }),
487
454
  /*#__PURE__*/ _jsx(Chip, {
488
455
  size: "small",
489
- color: STATUS_COLOR[job.status],
490
- label: STATUS_LABEL[job.status]
456
+ color: PHASE_COLOR[phase],
457
+ label: AI_JOB_PHASE_LABELS[phase]
491
458
  }),
492
459
  /*#__PURE__*/ _jsxs(Typography, {
493
460
  variant: "caption",
@@ -501,7 +468,7 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
501
468
  job.steps.length,
502
469
  " steps",
503
470
  current ? ` — ${current.name}` : '',
504
- job.creditsSpent > 0 ? ` · ${job.creditsSpent} credits` : ''
471
+ netCredits(job) > 0 ? ` · ${netCredits(job)} credits` : ''
505
472
  ]
506
473
  }),
507
474
  !terminal && /*#__PURE__*/ _jsx(Button, {
@@ -525,6 +492,16 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
525
492
  component: "div",
526
493
  children: job.error
527
494
  }),
495
+ primary ? /*#__PURE__*/ _jsx(Button, {
496
+ size: "small",
497
+ variant: "contained",
498
+ component: AppLink,
499
+ href: primary.href,
500
+ sx: {
501
+ mt: 0.5
502
+ },
503
+ children: primary.label
504
+ }) : null,
528
505
  job.outputs.map((output, index)=>{
529
506
  const href = aiJobOutputHref(output, orgSlug);
530
507
  const navigation = aiJobNavigationProposal(output);
@@ -591,7 +568,8 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
591
568
  job: job,
592
569
  onResume: (target)=>void resume(target),
593
570
  busy: resuming === job.id,
594
- staff: isStaff
571
+ staff: isStaff,
572
+ user: user
595
573
  })
596
574
  ]
597
575
  }, job.id);
@@ -608,10 +586,10 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
608
586
  orgId: orgId,
609
587
  orgSlug: orgSlug,
610
588
  hostId: (insight == null ? void 0 : insight.host) ? hostId != null ? hostId : null : null,
611
- host: (_ref1 = insight == null ? void 0 : insight.host) != null ? _ref1 : null,
612
- surface: (_ref2 = insight == null ? void 0 : insight.surface) != null ? _ref2 : 'analytics',
589
+ host: (_ref4 = insight == null ? void 0 : insight.host) != null ? _ref4 : null,
590
+ surface: (_ref5 = insight == null ? void 0 : insight.surface) != null ? _ref5 : 'analytics',
613
591
  user: user,
614
- uid: (_ref3 = user == null ? void 0 : user.uid) != null ? _ref3 : null,
592
+ uid: (_ref6 = user == null ? void 0 : user.uid) != null ? _ref6 : null,
615
593
  jobId: insightOpen === 'ask' ? null : insightOpen
616
594
  }),
617
595
  hostId && /*#__PURE__*/ _jsx(AiPageBriefDialog, {
@@ -622,7 +600,9 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
622
600
  },
623
601
  orgId: orgId,
624
602
  hostId: hostId,
625
- user: user
603
+ user: user,
604
+ orgSlug: orgSlug,
605
+ isStaff: isStaff
626
606
  })
627
607
  ]
628
608
  });