@carlonicora/nextjs-jsonapi 3.9.0 → 3.11.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.
- package/dist/BlockNoteEditor-K55A4FS2.css +30 -0
- package/dist/BlockNoteEditor-K55A4FS2.css.map +1 -0
- package/dist/{BlockNoteEditor-RGMU2O24.mjs → BlockNoteEditor-NBHDEZ7W.mjs} +4 -4
- package/dist/{BlockNoteEditor-WY4LFMXQ.js → BlockNoteEditor-XOVNTXDY.js} +19 -19
- package/dist/{BlockNoteEditor-WY4LFMXQ.js.map → BlockNoteEditor-XOVNTXDY.js.map} +1 -1
- package/dist/{BlockNoteViewer-6N2GQWTI.js → BlockNoteViewer-C2UUOBAF.js} +17 -3
- package/dist/BlockNoteViewer-C2UUOBAF.js.map +1 -0
- package/dist/{BlockNoteViewer-IBQRJ3WM.mjs → BlockNoteViewer-TYLVTIUD.mjs} +17 -3
- package/dist/BlockNoteViewer-TYLVTIUD.mjs.map +1 -0
- package/dist/billing/index.css +22 -21
- package/dist/billing/index.css.map +1 -1
- package/dist/billing/index.js +398 -398
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-UYLS3LJW.js → chunk-6J2SQZ3G.js} +11 -1
- package/dist/chunk-6J2SQZ3G.js.map +1 -0
- package/dist/{chunk-2D4MFVDV.mjs → chunk-IMSIVIIC.mjs} +2 -2
- package/dist/{chunk-HDZ2BL6D.mjs → chunk-M5QAHSCY.mjs} +11 -1
- package/dist/{chunk-HDZ2BL6D.mjs.map → chunk-M5QAHSCY.mjs.map} +1 -1
- package/dist/{chunk-TULJCLZA.js → chunk-V6CGCARD.js} +7 -7
- package/dist/{chunk-TULJCLZA.js.map → chunk-V6CGCARD.js.map} +1 -1
- package/dist/{chunk-7PNGCZES.mjs → chunk-XNGI4HDZ.mjs} +1529 -1286
- package/dist/chunk-XNGI4HDZ.mjs.map +1 -0
- package/dist/{chunk-DOLEXUO3.js → chunk-YN4LOVL4.js} +1301 -1058
- package/dist/chunk-YN4LOVL4.js.map +1 -0
- package/dist/client/index.css +22 -21
- package/dist/client/index.css.map +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.css +22 -21
- package/dist/components/index.css.map +1 -1
- package/dist/components/index.d.mts +46 -14
- package/dist/components/index.d.ts +46 -14
- package/dist/components/index.js +8 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +7 -3
- package/dist/contexts/index.css +22 -21
- package/dist/contexts/index.css.map +1 -1
- package/dist/contexts/index.d.mts +28 -4
- package/dist/contexts/index.d.ts +28 -4
- package/dist/contexts/index.js +10 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +9 -3
- package/dist/core/index.d.mts +13 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.css +22 -21
- package/dist/features/help/index.css.map +1 -1
- package/dist/features/help/index.d.mts +6 -1
- package/dist/features/help/index.d.ts +6 -1
- package/dist/features/help/index.js +39 -38
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +4 -3
- package/dist/features/tokenusage/index.css +22 -21
- package/dist/features/tokenusage/index.css.map +1 -1
- package/dist/features/tokenusage/index.js +102 -102
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{onboarding.interface-Djyl9qYu.d.mts → onboarding.interface-DVUOUC4y.d.mts} +4 -1
- package/dist/{onboarding.interface-Djyl9qYu.d.ts → onboarding.interface-DVUOUC4y.d.ts} +4 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +2 -2
- package/src/components/containers/RoundPageContainer.tsx +2 -1
- package/src/components/containers/RoundPageContainerTitle.tsx +6 -1
- package/src/components/containers/TabsContainer.tsx +1 -0
- package/src/components/forms/EditorSheet.tsx +2 -1
- package/src/components/grids/ContentListGrid.tsx +1 -1
- package/src/components/navigations/Breadcrumb.tsx +21 -9
- package/src/components/navigations/Header.tsx +3 -1
- package/src/components/navigations/MobileNavigationBar.tsx +1 -0
- package/src/components/tables/ContentListTable.tsx +1 -1
- package/src/components/tables/ContentTableSearch.tsx +1 -0
- package/src/components/viewers/BlockNoteViewer.tsx +13 -2
- package/src/components/viewers/BlockNoteViewerContainer.tsx +4 -1
- package/src/contexts/BreadcrumbRootContext.tsx +28 -0
- package/src/contexts/index.ts +1 -0
- package/src/features/assistant/components/index.ts +2 -0
- package/src/features/assistant/components/parts/AssistantComposer.tsx +1 -1
- package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +2 -2
- package/src/features/assistant-action/components/ApprovalActionCard.tsx +325 -4
- package/src/features/assistant-action/components/__tests__/ApprovalActionCard.spec.tsx +268 -0
- package/src/features/assistant-action/data/AssistantAction.ts +12 -0
- package/src/features/assistant-action/data/AssistantActionInterface.ts +9 -0
- package/src/features/assistant-action/data/__tests__/AssistantAction.spec.ts +11 -0
- package/src/features/help/index.ts +1 -0
- package/src/features/onboarding/components/OnboardingCard.tsx +1 -2
- package/src/features/onboarding/contexts/OnboardingContext.spec.tsx +191 -0
- package/src/features/onboarding/contexts/OnboardingContext.tsx +176 -125
- package/src/features/onboarding/interfaces/onboarding.interface.ts +4 -0
- package/src/features/onboarding/styles/onboarding.css +25 -30
- package/src/features/tokenusage/components/TokenUsageReportFilterBar.tsx +1 -1
- package/dist/BlockNoteEditor-YOAJRPWU.css +0 -29
- package/dist/BlockNoteEditor-YOAJRPWU.css.map +0 -1
- package/dist/BlockNoteViewer-6N2GQWTI.js.map +0 -1
- package/dist/BlockNoteViewer-IBQRJ3WM.mjs.map +0 -1
- package/dist/chunk-7PNGCZES.mjs.map +0 -1
- package/dist/chunk-DOLEXUO3.js.map +0 -1
- package/dist/chunk-UYLS3LJW.js.map +0 -1
- /package/dist/{BlockNoteEditor-RGMU2O24.mjs.map → BlockNoteEditor-NBHDEZ7W.mjs.map} +0 -0
- /package/dist/{chunk-2D4MFVDV.mjs.map → chunk-IMSIVIIC.mjs.map} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ShieldAlert } from "lucide-react";
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
|
-
import { useEffect, useState } from "react";
|
|
5
|
+
import { Fragment, useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { MicroLabel } from "../../../components/typography";
|
|
7
7
|
import { Badge, Button } from "../../../shadcnui";
|
|
8
8
|
import type { AssistantMessageInterface } from "../../assistant-message/data/AssistantMessageInterface";
|
|
@@ -35,27 +35,278 @@ const STATUS_LABEL_KEY: Record<AssistantActionStatus, string> = {
|
|
|
35
35
|
expired: "features.assistant.approval.status.expired",
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* One referenced record, already resolved to a display name by the API.
|
|
40
|
+
* `id` exists for React keys only — it is NEVER rendered.
|
|
41
|
+
*/
|
|
42
|
+
interface ProposalRef {
|
|
43
|
+
id: string;
|
|
44
|
+
type: string;
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The operator's presentation payload (`AssistantAction.proposal`). Every
|
|
50
|
+
* referenced record arrives resolved to a name, so the card never has to show
|
|
51
|
+
* an id — an unknown or out-of-scope record is resolved to "(not found)" by
|
|
52
|
+
* the API.
|
|
53
|
+
*/
|
|
54
|
+
interface OperatorActionProposal {
|
|
55
|
+
/** JSON:API type of the record being written. */
|
|
56
|
+
type: string;
|
|
57
|
+
/** update / delete / link / unlink: the target record. Absent on create. */
|
|
58
|
+
target?: ProposalRef;
|
|
59
|
+
/** create / update: the field values being written. */
|
|
60
|
+
attributes?: Record<string, unknown>;
|
|
61
|
+
/** create: related records, resolved. The run's scope relationship is stripped by the API. */
|
|
62
|
+
relationships?: Record<string, ProposalRef[]>;
|
|
63
|
+
/** link / unlink: the relationship name… */
|
|
64
|
+
relationship?: string;
|
|
65
|
+
/** …and the records being linked or unlinked. */
|
|
66
|
+
targets?: ProposalRef[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A parsed `toolArgs` payload, used only as the legacy fallback for actions
|
|
71
|
+
* created before `proposal` existed.
|
|
72
|
+
*
|
|
73
|
+
* NOTE on the key name: `create_entity` / `update_entity` declare the field
|
|
74
|
+
* values under `fields` (see `agents/operator/tools/entity-write.tools.ts`),
|
|
75
|
+
* while other producers use the JSON:API-shaped `attributes`. Both are read so
|
|
76
|
+
* the card never silently degrades to the bare summary. Relationships are NOT
|
|
77
|
+
* read from here: `toolArgs` carries ids only, and no id is ever rendered.
|
|
78
|
+
*/
|
|
79
|
+
interface LegacyProposal {
|
|
80
|
+
type: string;
|
|
81
|
+
attributes: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ProposalRow {
|
|
85
|
+
key: string;
|
|
86
|
+
label: string;
|
|
87
|
+
value: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Entity label from a JSON:API type: drop a trailing "s", upper-case the first
|
|
92
|
+
* letter. Short types (three letters or fewer after singularising) are
|
|
93
|
+
* acronyms in this domain — "npcs" → "NPC", "pcs" → "PC" — so they are
|
|
94
|
+
* upper-cased whole.
|
|
95
|
+
*/
|
|
96
|
+
function humaniseType(type: string): string {
|
|
97
|
+
const singular = type.endsWith("s") ? type.slice(0, -1) : type;
|
|
98
|
+
if (singular.length <= 3) return singular.toUpperCase();
|
|
99
|
+
return singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `tldr` → `Tldr`, `publicDescription` → `Public description`. */
|
|
103
|
+
function humaniseKey(key: string): string {
|
|
104
|
+
const words = key
|
|
105
|
+
.replace(/[_-]+/g, " ")
|
|
106
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
107
|
+
.trim();
|
|
108
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Plain text of a BlockNote document, without importing BlockNote: walk the
|
|
113
|
+
* blocks, concatenate the text runs of each block's `content`, recurse into
|
|
114
|
+
* `children`, and join the blocks with newlines.
|
|
115
|
+
*/
|
|
116
|
+
function blockNoteText(blocks: unknown[]): string {
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
const walk = (nodes: unknown[]) => {
|
|
119
|
+
for (const node of nodes) {
|
|
120
|
+
if (!node || typeof node !== "object") continue;
|
|
121
|
+
const block = node as { content?: unknown; children?: unknown };
|
|
122
|
+
const runs = Array.isArray(block.content)
|
|
123
|
+
? block.content
|
|
124
|
+
.map((run) =>
|
|
125
|
+
run && typeof run === "object" && typeof (run as { text?: unknown }).text === "string"
|
|
126
|
+
? (run as { text: string }).text
|
|
127
|
+
: "",
|
|
128
|
+
)
|
|
129
|
+
.join("")
|
|
130
|
+
: "";
|
|
131
|
+
if (runs.trim()) lines.push(runs);
|
|
132
|
+
if (Array.isArray(block.children)) walk(block.children);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
walk(blocks);
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Returns the blocks when `value` parses as a BlockNote document (an array of `{ type }` blocks). */
|
|
140
|
+
function asBlockNoteDocument(value: string): unknown[] | undefined {
|
|
141
|
+
if (!value.trimStart().startsWith("[")) return undefined;
|
|
142
|
+
try {
|
|
143
|
+
const parsed: unknown = JSON.parse(value);
|
|
144
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return undefined;
|
|
145
|
+
const everyBlock = parsed.every(
|
|
146
|
+
(block) => !!block && typeof block === "object" && typeof (block as { type?: unknown }).type === "string",
|
|
147
|
+
);
|
|
148
|
+
return everyBlock ? parsed : undefined;
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Renders one attribute value as text. `undefined` means "skip this row". */
|
|
155
|
+
function formatAttribute(value: unknown, yes: string, no: string): string | undefined {
|
|
156
|
+
if (value === null || value === undefined) return undefined;
|
|
157
|
+
if (typeof value === "boolean") return value ? yes : no;
|
|
158
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : undefined;
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
const joined = value
|
|
161
|
+
.map((entry) =>
|
|
162
|
+
entry === null || entry === undefined ? "" : typeof entry === "object" ? JSON.stringify(entry) : String(entry),
|
|
163
|
+
)
|
|
164
|
+
.filter((entry) => entry !== "")
|
|
165
|
+
.join(", ");
|
|
166
|
+
return joined === "" ? undefined : joined;
|
|
167
|
+
}
|
|
168
|
+
if (typeof value === "string") {
|
|
169
|
+
if (value.trim() === "") return undefined;
|
|
170
|
+
const document = asBlockNoteDocument(value);
|
|
171
|
+
if (!document) return value;
|
|
172
|
+
const text = blockNoteText(document);
|
|
173
|
+
return text.trim() === "" ? undefined : text;
|
|
174
|
+
}
|
|
175
|
+
if (typeof value === "object") {
|
|
176
|
+
const serialised = JSON.stringify(value);
|
|
177
|
+
return serialised === "{}" ? undefined : serialised;
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Joins the display names of resolved records. `undefined` means "skip this row". */
|
|
183
|
+
function formatRefs(refs: ProposalRef[]): string | undefined {
|
|
184
|
+
const labels = refs.map((ref) => ref.label).filter((label) => label.trim() !== "");
|
|
185
|
+
return labels.length ? labels.join(", ") : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A `{ id, type, label }` record, or `undefined` when the entry is not one. */
|
|
189
|
+
function asRef(value: unknown): ProposalRef | undefined {
|
|
190
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
191
|
+
const { id, type, label } = value as { id?: unknown; type?: unknown; label?: unknown };
|
|
192
|
+
if (typeof label !== "string") return undefined;
|
|
193
|
+
return {
|
|
194
|
+
id: typeof id === "string" ? id : "",
|
|
195
|
+
type: typeof type === "string" ? type : "",
|
|
196
|
+
label,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function asRefs(value: unknown): ProposalRef[] {
|
|
201
|
+
if (!Array.isArray(value)) return [];
|
|
202
|
+
return value.flatMap((entry) => {
|
|
203
|
+
const ref = asRef(entry);
|
|
204
|
+
return ref ? [ref] : [];
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parses the action's `proposal` — the API's presentation payload, where every
|
|
210
|
+
* referenced record already carries its display name. Returns `undefined` when
|
|
211
|
+
* the payload is absent or unparseable, so the card can fall back to the
|
|
212
|
+
* legacy `toolArgs` attributes.
|
|
213
|
+
*/
|
|
214
|
+
function parseOperatorProposal(proposal: string | undefined): OperatorActionProposal | undefined {
|
|
215
|
+
if (!proposal) return undefined;
|
|
216
|
+
let parsed: unknown;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(proposal);
|
|
219
|
+
} catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
223
|
+
|
|
224
|
+
const payload = parsed as Record<string, unknown>;
|
|
225
|
+
const attributes =
|
|
226
|
+
payload.attributes && typeof payload.attributes === "object" && !Array.isArray(payload.attributes)
|
|
227
|
+
? (payload.attributes as Record<string, unknown>)
|
|
228
|
+
: undefined;
|
|
229
|
+
|
|
230
|
+
const relationships: Record<string, ProposalRef[]> = {};
|
|
231
|
+
if (payload.relationships && typeof payload.relationships === "object" && !Array.isArray(payload.relationships)) {
|
|
232
|
+
for (const [key, value] of Object.entries(payload.relationships as Record<string, unknown>)) {
|
|
233
|
+
relationships[key] = asRefs(value);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
type: typeof payload.type === "string" ? payload.type : "",
|
|
239
|
+
target: asRef(payload.target),
|
|
240
|
+
attributes,
|
|
241
|
+
relationships,
|
|
242
|
+
relationship: typeof payload.relationship === "string" ? payload.relationship : undefined,
|
|
243
|
+
targets: asRefs(payload.targets),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Legacy fallback for actions created before `proposal` existed: parses the
|
|
249
|
+
* field values out of `toolArgs`. Returns `undefined` whenever the payload is
|
|
250
|
+
* absent, unparseable or carries no field values — the card then falls back to
|
|
251
|
+
* the summary alone, which must always still render with its buttons.
|
|
252
|
+
*
|
|
253
|
+
* Relationships are deliberately ignored here: `toolArgs` names them by id
|
|
254
|
+
* only, and an id must never reach the screen.
|
|
255
|
+
*/
|
|
256
|
+
function parseLegacyProposal(toolArgs: string | undefined): LegacyProposal | undefined {
|
|
257
|
+
if (!toolArgs) return undefined;
|
|
258
|
+
let parsed: unknown;
|
|
259
|
+
try {
|
|
260
|
+
parsed = JSON.parse(toolArgs);
|
|
261
|
+
} catch {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
265
|
+
|
|
266
|
+
const payload = parsed as Record<string, unknown>;
|
|
267
|
+
const attributes = payload.attributes ?? payload.fields;
|
|
268
|
+
if (!attributes || typeof attributes !== "object" || Array.isArray(attributes)) return undefined;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
type: typeof payload.type === "string" ? payload.type : "",
|
|
272
|
+
attributes: attributes as Record<string, unknown>,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
38
276
|
interface Props {
|
|
39
277
|
/** Id of the pending AssistantAction linked to the approval-request message. */
|
|
40
278
|
actionId: string;
|
|
41
279
|
/** Human-readable summary fallback (the approval-request message content). */
|
|
42
280
|
summary?: string;
|
|
281
|
+
/**
|
|
282
|
+
* Relationship keys from the proposal that must not be rendered as rows.
|
|
283
|
+
*
|
|
284
|
+
* The write tools discard the model's scope relationship and force the run's
|
|
285
|
+
* own scope, so showing it is noise at best and wrong at worst (the proposal
|
|
286
|
+
* may name a different record than the one that will actually be written).
|
|
287
|
+
* The host app names the keys it forces — e.g. `["campaign"]`.
|
|
288
|
+
*/
|
|
289
|
+
hiddenRelationshipKeys?: string[];
|
|
43
290
|
/** Invoked with the resumed assistant message returned by approve/deny. */
|
|
44
291
|
onResolved?: (message: AssistantMessageInterface) => void;
|
|
45
292
|
}
|
|
46
293
|
|
|
47
294
|
/**
|
|
48
295
|
* Renders an `approval-request` assistant message as an action card:
|
|
49
|
-
* the destructive-tool summary
|
|
50
|
-
* only actionable while the
|
|
296
|
+
* the destructive-tool summary, the proposed record (what the assistant wants
|
|
297
|
+
* to write), plus Approve / Deny buttons. Buttons are only actionable while the
|
|
298
|
+
* underlying AssistantAction is `pending`.
|
|
51
299
|
*
|
|
52
300
|
* NOTE: plain `<Button>` elements on purpose — never wrapped inside any
|
|
53
301
|
* Base UI trigger component (no nested buttons, no `asChild`).
|
|
54
302
|
*/
|
|
55
|
-
export function ApprovalActionCard({ actionId, summary, onResolved }: Props) {
|
|
303
|
+
export function ApprovalActionCard({ actionId, summary, onResolved, hiddenRelationshipKeys }: Props) {
|
|
56
304
|
const t = useTranslations();
|
|
57
305
|
const [status, setStatus] = useState<AssistantActionStatus | undefined>(undefined);
|
|
58
306
|
const [actionSummary, setActionSummary] = useState<string | undefined>(undefined);
|
|
307
|
+
const [toolName, setToolName] = useState<string | undefined>(undefined);
|
|
308
|
+
const [toolArgs, setToolArgs] = useState<string | undefined>(undefined);
|
|
309
|
+
const [actionProposal, setActionProposal] = useState<string | undefined>(undefined);
|
|
59
310
|
const [busy, setBusy] = useState<boolean>(false);
|
|
60
311
|
const [failed, setFailed] = useState<boolean>(false);
|
|
61
312
|
|
|
@@ -67,6 +318,9 @@ export function ApprovalActionCard({ actionId, summary, onResolved }: Props) {
|
|
|
67
318
|
if (cancelled || !action) return;
|
|
68
319
|
setStatus(action.status);
|
|
69
320
|
setActionSummary(action.summary);
|
|
321
|
+
setToolName(action.toolName);
|
|
322
|
+
setToolArgs(action.toolArgs);
|
|
323
|
+
setActionProposal(action.proposal);
|
|
70
324
|
} catch (error) {
|
|
71
325
|
console.error(`ApprovalActionCard: failed to fetch assistant action ${actionId}`, error);
|
|
72
326
|
if (!cancelled) setFailed(true);
|
|
@@ -77,6 +331,53 @@ export function ApprovalActionCard({ actionId, summary, onResolved }: Props) {
|
|
|
77
331
|
};
|
|
78
332
|
}, [actionId]);
|
|
79
333
|
|
|
334
|
+
// The API's resolved payload wins; `toolArgs` is only the legacy fallback
|
|
335
|
+
// (attributes alone — it names related records by id, which is never shown).
|
|
336
|
+
const presented = useMemo(() => parseOperatorProposal(actionProposal), [actionProposal]);
|
|
337
|
+
const legacy = useMemo(() => (presented ? undefined : parseLegacyProposal(toolArgs)), [presented, toolArgs]);
|
|
338
|
+
const isUpdate = toolName === "update_entity";
|
|
339
|
+
const entityLabel = humaniseType(presented?.type ?? legacy?.type ?? "");
|
|
340
|
+
const targetLabel = presented?.target?.label ?? "";
|
|
341
|
+
|
|
342
|
+
const rows = useMemo<ProposalRow[]>(() => {
|
|
343
|
+
const yes = t("features.assistant.approval.proposal.yes");
|
|
344
|
+
const no = t("features.assistant.approval.proposal.no");
|
|
345
|
+
const attributes = presented?.attributes ?? legacy?.attributes;
|
|
346
|
+
const attributeRows = Object.entries(attributes ?? {}).flatMap<ProposalRow>(([key, value]) => {
|
|
347
|
+
const text = formatAttribute(value, yes, no);
|
|
348
|
+
return text === undefined ? [] : [{ key: `attribute:${key}`, label: humaniseKey(key), value: text }];
|
|
349
|
+
});
|
|
350
|
+
if (!presented) return attributeRows;
|
|
351
|
+
|
|
352
|
+
const hidden = new Set(hiddenRelationshipKeys ?? []);
|
|
353
|
+
const relationshipRows = Object.entries(presented.relationships ?? {}).flatMap<ProposalRow>(([key, refs]) => {
|
|
354
|
+
if (hidden.has(key)) return [];
|
|
355
|
+
const text = formatRefs(refs);
|
|
356
|
+
return text === undefined ? [] : [{ key: `relationship:${key}`, label: humaniseKey(key), value: text }];
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const linkRows: ProposalRow[] = [];
|
|
360
|
+
if (presented.relationship) {
|
|
361
|
+
linkRows.push({
|
|
362
|
+
key: "link:relationship",
|
|
363
|
+
label: t("features.assistant.approval.proposal.relationship"),
|
|
364
|
+
value: humaniseKey(presented.relationship),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const targetsText = formatRefs(presented.targets ?? []);
|
|
368
|
+
if (targetsText !== undefined) {
|
|
369
|
+
linkRows.push({
|
|
370
|
+
key: "link:records",
|
|
371
|
+
label: t("features.assistant.approval.proposal.records"),
|
|
372
|
+
value: targetsText,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return [...attributeRows, ...relationshipRows, ...linkRows];
|
|
377
|
+
}, [presented, legacy, t, hiddenRelationshipKeys]);
|
|
378
|
+
|
|
379
|
+
const hasProposal = rows.length > 0;
|
|
380
|
+
|
|
80
381
|
const actionable = status === "pending" && !busy && !failed;
|
|
81
382
|
|
|
82
383
|
const resolve = async (kind: "approve" | "deny") => {
|
|
@@ -109,6 +410,26 @@ export function ApprovalActionCard({ actionId, summary, onResolved }: Props) {
|
|
|
109
410
|
)}
|
|
110
411
|
</MicroLabel>
|
|
111
412
|
<div className="text-foreground text-sm leading-relaxed">{actionSummary ?? summary}</div>
|
|
413
|
+
{hasProposal && (
|
|
414
|
+
<div className="border-border/60 flex flex-col gap-2 rounded-lg border border-dashed p-3">
|
|
415
|
+
<MicroLabel className="text-muted-foreground flex flex-wrap items-baseline gap-x-2">
|
|
416
|
+
<span>
|
|
417
|
+
{isUpdate
|
|
418
|
+
? t("features.assistant.approval.proposal.updateHeading", { entity: entityLabel })
|
|
419
|
+
: t("features.assistant.approval.proposal.heading", { entity: entityLabel })}
|
|
420
|
+
</span>
|
|
421
|
+
{targetLabel !== "" && <span className="text-foreground tracking-normal normal-case">{targetLabel}</span>}
|
|
422
|
+
</MicroLabel>
|
|
423
|
+
<dl className="grid grid-cols-1 gap-x-4 gap-y-1.5 sm:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]">
|
|
424
|
+
{rows.map((row) => (
|
|
425
|
+
<Fragment key={row.key}>
|
|
426
|
+
<dt className="text-muted-foreground text-xs leading-relaxed break-words sm:text-sm">{row.label}</dt>
|
|
427
|
+
<dd className="text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap">{row.value}</dd>
|
|
428
|
+
</Fragment>
|
|
429
|
+
))}
|
|
430
|
+
</dl>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
112
433
|
{failed && (
|
|
113
434
|
<div role="alert" className="text-destructive text-xs/relaxed">
|
|
114
435
|
{t("features.assistant.approval.error")}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ApprovalActionCard } from "../ApprovalActionCard";
|
|
4
|
+
|
|
5
|
+
const findOne = vi.fn();
|
|
6
|
+
const approve = vi.fn();
|
|
7
|
+
const deny = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock("../../data/AssistantActionService", () => ({
|
|
10
|
+
AssistantActionService: {
|
|
11
|
+
findOne: (params: { id: string }) => findOne(params),
|
|
12
|
+
approve: (params: { id: string }) => approve(params),
|
|
13
|
+
deny: (params: { id: string }) => deny(params),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
/** Any uuid-ish run of hex — no id may ever reach the screen. */
|
|
18
|
+
const UUID_LIKE = /[0-9a-f]{8}-[0-9a-f]{4}-/;
|
|
19
|
+
|
|
20
|
+
const CREATE_TOOL_ARGS = JSON.stringify({
|
|
21
|
+
type: "npcs",
|
|
22
|
+
attributes: { name: "Marcus", tldr: "A nerd with bouts of rage" },
|
|
23
|
+
relationships: { related: { data: [{ type: "npcs", id: "042ec319-9d3c-4f0a-9a6c-1a2b3c4d5e6f" }] } },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const CREATE_PROPOSAL = JSON.stringify({
|
|
27
|
+
type: "npcs",
|
|
28
|
+
attributes: { name: "Marcus", tldr: "A nerd with bouts of rage" },
|
|
29
|
+
relationships: {
|
|
30
|
+
related: [{ id: "042ec319-9d3c-4f0a-9a6c-1a2b3c4d5e6f", type: "npcs", label: "Zoe" }],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function makeAction(overrides: Record<string, unknown> = {}) {
|
|
35
|
+
return {
|
|
36
|
+
id: "act-1",
|
|
37
|
+
status: "pending",
|
|
38
|
+
toolName: "create_entity",
|
|
39
|
+
toolArgs: CREATE_TOOL_ARGS,
|
|
40
|
+
proposal: CREATE_PROPOSAL,
|
|
41
|
+
summary: 'Create a new npcs record named "Marcus".',
|
|
42
|
+
...overrides,
|
|
43
|
+
} as any;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("ApprovalActionCard", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
findOne.mockReset();
|
|
49
|
+
approve.mockReset();
|
|
50
|
+
deny.mockReset();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders the proposed record: the attributes and the related records by name", async () => {
|
|
54
|
+
findOne.mockResolvedValue(makeAction());
|
|
55
|
+
|
|
56
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
57
|
+
|
|
58
|
+
// summary still renders
|
|
59
|
+
expect(await screen.findByText('Create a new npcs record named "Marcus".')).toBeInTheDocument();
|
|
60
|
+
// attributes
|
|
61
|
+
expect(await screen.findByText("Marcus")).toBeInTheDocument();
|
|
62
|
+
expect(screen.getByText("A nerd with bouts of rage")).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText("Name")).toBeInTheDocument();
|
|
64
|
+
// relationships: humanised key + the resolved display names, never ids
|
|
65
|
+
expect(screen.getByText("Related")).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText("Zoe")).toBeInTheDocument();
|
|
67
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
68
|
+
// buttons are actionable while the action is pending
|
|
69
|
+
expect(screen.getByRole("button", { name: "features.assistant.approval.approve" })).toBeEnabled();
|
|
70
|
+
expect(screen.getByRole("button", { name: "features.assistant.approval.deny" })).toBeEnabled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders booleans as Yes/No and numbers as-is", async () => {
|
|
74
|
+
findOne.mockResolvedValue(
|
|
75
|
+
makeAction({
|
|
76
|
+
proposal: JSON.stringify({
|
|
77
|
+
type: "factions",
|
|
78
|
+
attributes: { name: "The Cinder Pact", secret: true, rank: 3 },
|
|
79
|
+
}),
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
84
|
+
|
|
85
|
+
expect(await screen.findByText("The Cinder Pact")).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText("features.assistant.approval.proposal.yes")).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByText("3")).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders the plain text of a BlockNote document value", async () => {
|
|
91
|
+
const document = JSON.stringify([
|
|
92
|
+
{ id: "1", type: "paragraph", content: [{ type: "text", text: "First line" }], children: [] },
|
|
93
|
+
{
|
|
94
|
+
id: "2",
|
|
95
|
+
type: "paragraph",
|
|
96
|
+
content: [
|
|
97
|
+
{ type: "text", text: "Second " },
|
|
98
|
+
{ type: "text", text: "line" },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
findOne.mockResolvedValue(
|
|
103
|
+
makeAction({ proposal: JSON.stringify({ type: "npcs", attributes: { description: document } }) }),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
107
|
+
|
|
108
|
+
// identity normalizer: the default one collapses the newline the reducer emits
|
|
109
|
+
expect(await screen.findByText("First line\nSecond line", { normalizer: (text) => text })).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("skips null, undefined and empty attribute rows", async () => {
|
|
113
|
+
findOne.mockResolvedValue(
|
|
114
|
+
makeAction({
|
|
115
|
+
proposal: JSON.stringify({
|
|
116
|
+
type: "npcs",
|
|
117
|
+
attributes: { name: "Marcus", tldr: null, notes: "", tags: [] },
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
123
|
+
|
|
124
|
+
expect(await screen.findByText("Marcus")).toBeInTheDocument();
|
|
125
|
+
expect(screen.queryByText("Tldr")).not.toBeInTheDocument();
|
|
126
|
+
expect(screen.queryByText("Notes")).not.toBeInTheDocument();
|
|
127
|
+
expect(screen.queryByText("Tags")).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("shows the target record's name — never its id — for an update_entity proposal", async () => {
|
|
131
|
+
findOne.mockResolvedValue(
|
|
132
|
+
makeAction({
|
|
133
|
+
toolName: "update_entity",
|
|
134
|
+
summary: 'Update the npcs record "Marcus" (name).',
|
|
135
|
+
toolArgs: JSON.stringify({
|
|
136
|
+
type: "npcs",
|
|
137
|
+
id: "npc-9d3c4f0a-1a2b-4c4d-9a6c-042ec3199d3c",
|
|
138
|
+
attributes: { name: "Marcus the Calm" },
|
|
139
|
+
}),
|
|
140
|
+
proposal: JSON.stringify({
|
|
141
|
+
type: "npcs",
|
|
142
|
+
target: { id: "9d3c4f0a-1a2b-4c4d-9a6c-042ec3199d3c", type: "npcs", label: "Marcus" },
|
|
143
|
+
attributes: { name: "Marcus the Calm" },
|
|
144
|
+
}),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
149
|
+
|
|
150
|
+
expect(await screen.findByText("features.assistant.approval.proposal.updateHeading")).toBeInTheDocument();
|
|
151
|
+
expect(screen.getByText("Marcus")).toBeInTheDocument();
|
|
152
|
+
expect(screen.getByText("Marcus the Calm")).toBeInTheDocument();
|
|
153
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("shows the relationship name and the linked records for a link proposal", async () => {
|
|
157
|
+
findOne.mockResolvedValue(
|
|
158
|
+
makeAction({
|
|
159
|
+
toolName: "link_entities",
|
|
160
|
+
summary: 'Link "Zoe", "Bob" to the "related" relationship of the npcs record "Marcus".',
|
|
161
|
+
proposal: JSON.stringify({
|
|
162
|
+
type: "npcs",
|
|
163
|
+
target: { id: "9d3c4f0a-1a2b-4c4d-9a6c-042ec3199d3c", type: "npcs", label: "Marcus" },
|
|
164
|
+
relationship: "related",
|
|
165
|
+
targets: [
|
|
166
|
+
{ id: "042ec319-9d3c-4f0a-9a6c-1a2b3c4d5e6f", type: "npcs", label: "Zoe" },
|
|
167
|
+
{ id: "c81d6ac7-1a2b-4c4d-9a6c-042ec3199d3c", type: "npcs", label: "Bob" },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
174
|
+
|
|
175
|
+
expect(await screen.findByText("features.assistant.approval.proposal.relationship")).toBeInTheDocument();
|
|
176
|
+
expect(screen.getByText("Related")).toBeInTheDocument();
|
|
177
|
+
expect(screen.getByText("features.assistant.approval.proposal.records")).toBeInTheDocument();
|
|
178
|
+
expect(screen.getByText("Zoe, Bob")).toBeInTheDocument();
|
|
179
|
+
expect(screen.getByText("Marcus")).toBeInTheDocument();
|
|
180
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("shows the unlinked records by name for an unlink proposal", async () => {
|
|
184
|
+
findOne.mockResolvedValue(
|
|
185
|
+
makeAction({
|
|
186
|
+
toolName: "unlink_entities",
|
|
187
|
+
summary: 'Unlink "Zoe" from the "related" relationship of the npcs record "Marcus".',
|
|
188
|
+
proposal: JSON.stringify({
|
|
189
|
+
type: "npcs",
|
|
190
|
+
target: { id: "9d3c4f0a-1a2b-4c4d-9a6c-042ec3199d3c", type: "npcs", label: "Marcus" },
|
|
191
|
+
relationship: "related",
|
|
192
|
+
targets: [{ id: "042ec319-9d3c-4f0a-9a6c-1a2b3c4d5e6f", type: "npcs", label: "Zoe" }],
|
|
193
|
+
}),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
198
|
+
|
|
199
|
+
expect(await screen.findByText("Zoe")).toBeInTheDocument();
|
|
200
|
+
expect(screen.getByText("features.assistant.approval.proposal.records")).toBeInTheDocument();
|
|
201
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("hides the relationship rows named by hiddenRelationshipKeys, and shows them when it does not", async () => {
|
|
205
|
+
// The write tools force the run's own scope and discard the model's, so
|
|
206
|
+
// the host app hides that relationship rather than showing a record the
|
|
207
|
+
// write may not even end up using.
|
|
208
|
+
const proposal = JSON.stringify({
|
|
209
|
+
type: "npcs",
|
|
210
|
+
attributes: { name: "Marcus" },
|
|
211
|
+
relationships: {
|
|
212
|
+
campaign: [{ id: "c81d6ac7-1a2b-4c4d-9a6c-042ec3199d3c", type: "campaigns", label: "The Ashen Crown" }],
|
|
213
|
+
related: [{ id: "042ec319-9d3c-4f0a-9a6c-1a2b3c4d5e6f", type: "npcs", label: "Zoe" }],
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
findOne.mockResolvedValue(makeAction({ proposal }));
|
|
217
|
+
|
|
218
|
+
const hidden = render(<ApprovalActionCard actionId="act-1" hiddenRelationshipKeys={["campaign"]} />);
|
|
219
|
+
|
|
220
|
+
expect(await screen.findByText("Marcus")).toBeInTheDocument();
|
|
221
|
+
expect(screen.queryByText("Campaign")).not.toBeInTheDocument();
|
|
222
|
+
expect(screen.queryByText("The Ashen Crown")).not.toBeInTheDocument();
|
|
223
|
+
// the relationships that are not listed keep rendering
|
|
224
|
+
expect(screen.getByText("Related")).toBeInTheDocument();
|
|
225
|
+
expect(screen.getByText("Zoe")).toBeInTheDocument();
|
|
226
|
+
|
|
227
|
+
hidden.unmount();
|
|
228
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
229
|
+
|
|
230
|
+
expect(await screen.findByText("Campaign")).toBeInTheDocument();
|
|
231
|
+
expect(screen.getByText("The Ashen Crown")).toBeInTheDocument();
|
|
232
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("falls back to the toolArgs attributes — and to no relationship rows — for a legacy action", async () => {
|
|
236
|
+
findOne.mockResolvedValue(
|
|
237
|
+
makeAction({
|
|
238
|
+
proposal: "",
|
|
239
|
+
toolArgs: JSON.stringify({
|
|
240
|
+
type: "npcs",
|
|
241
|
+
fields: { name: "Marcus", tldr: "A nerd with bouts of rage" },
|
|
242
|
+
relationships: { related: { data: [{ type: "npcs", id: "042ec319-9d3c-4f0a-9a6c-1a2b3c4d5e6f" }] } },
|
|
243
|
+
}),
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
248
|
+
|
|
249
|
+
// attributes still render (`fields` is the key the create_entity tool emits)
|
|
250
|
+
expect(await screen.findByText("Marcus")).toBeInTheDocument();
|
|
251
|
+
expect(screen.getByText("A nerd with bouts of rage")).toBeInTheDocument();
|
|
252
|
+
// relationships from toolArgs carry ids only — no row, and no id on screen
|
|
253
|
+
expect(screen.queryByText("Related")).not.toBeInTheDocument();
|
|
254
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("still renders the summary and the buttons when neither payload parses", async () => {
|
|
258
|
+
findOne.mockResolvedValue(makeAction({ proposal: "{not-json", toolArgs: "{not-json" }));
|
|
259
|
+
|
|
260
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
261
|
+
|
|
262
|
+
expect(await screen.findByText('Create a new npcs record named "Marcus".')).toBeInTheDocument();
|
|
263
|
+
expect(screen.getByRole("button", { name: "features.assistant.approval.approve" })).toBeEnabled();
|
|
264
|
+
expect(screen.getByRole("button", { name: "features.assistant.approval.deny" })).toBeEnabled();
|
|
265
|
+
expect(screen.queryByText("features.assistant.approval.proposal.heading")).not.toBeInTheDocument();
|
|
266
|
+
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -4,6 +4,8 @@ import { AssistantActionInput, AssistantActionInterface, AssistantActionStatus }
|
|
|
4
4
|
export class AssistantAction extends AbstractApiData implements AssistantActionInterface {
|
|
5
5
|
private _status?: AssistantActionStatus;
|
|
6
6
|
private _toolName?: string;
|
|
7
|
+
private _toolArgs?: string;
|
|
8
|
+
private _proposal?: string;
|
|
7
9
|
private _summary?: string;
|
|
8
10
|
private _resolvedAt?: Date;
|
|
9
11
|
private _expiresAt?: Date;
|
|
@@ -17,6 +19,14 @@ export class AssistantAction extends AbstractApiData implements AssistantActionI
|
|
|
17
19
|
return this._toolName ?? "";
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
get toolArgs(): string {
|
|
23
|
+
return this._toolArgs ?? "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get proposal(): string {
|
|
27
|
+
return this._proposal ?? "";
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
get summary(): string {
|
|
21
31
|
return this._summary ?? "";
|
|
22
32
|
}
|
|
@@ -34,6 +44,8 @@ export class AssistantAction extends AbstractApiData implements AssistantActionI
|
|
|
34
44
|
|
|
35
45
|
this._status = data.jsonApi.attributes.status;
|
|
36
46
|
this._toolName = data.jsonApi.attributes.toolName;
|
|
47
|
+
this._toolArgs = data.jsonApi.attributes.toolArgs;
|
|
48
|
+
this._proposal = data.jsonApi.attributes.proposal;
|
|
37
49
|
this._summary = data.jsonApi.attributes.summary;
|
|
38
50
|
this._resolvedAt = data.jsonApi.attributes.resolvedAt ? new Date(data.jsonApi.attributes.resolvedAt) : undefined;
|
|
39
51
|
this._expiresAt = data.jsonApi.attributes.expiresAt ? new Date(data.jsonApi.attributes.expiresAt) : undefined;
|