@zeniai/client-epic-state 5.2.4 → 5.2.6
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/lib/epic.js +14 -4
- package/lib/esm/epic.js +14 -4
- package/lib/esm/index.js +4 -3
- package/lib/esm/view/accountList/accountListReducer.js +1 -0
- package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +411 -9
- package/lib/esm/view/aiCfoView/aiCfoViewSelector.js +49 -0
- package/lib/esm/view/aiCfoView/epics/deleteSkillEpic.js +23 -0
- package/lib/esm/view/aiCfoView/epics/fetchSkillDetailEpic.js +35 -0
- package/lib/esm/view/aiCfoView/epics/fetchSkillVersionBodyEpic.js +45 -0
- package/lib/esm/view/aiCfoView/epics/fetchSkillVersionsEpic.js +37 -0
- package/lib/esm/view/aiCfoView/epics/fetchSkillsEpic.js +11 -6
- package/lib/esm/view/aiCfoView/epics/saveSkillEpic.js +72 -0
- package/lib/esm/view/aiCfoView/epics/setSkillVisibilityEpic.js +32 -0
- package/lib/esm/view/aiCfoView/epics/toSkillDetail.js +102 -0
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +8 -0
- package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +29 -0
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewPayload.js +0 -23
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewReducer.js +3 -40
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +124 -6
- package/lib/index.d.ts +7 -5
- package/lib/index.js +31 -14
- package/lib/view/accountList/accountListReducer.js +1 -0
- package/lib/view/accountList/accountListState.d.ts +1 -1
- package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +85 -0
- package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +67 -2
- package/lib/view/aiCfoView/aiCfoViewReducer.js +413 -10
- package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +7 -1
- package/lib/view/aiCfoView/aiCfoViewSelector.js +54 -0
- package/lib/view/aiCfoView/aiCfoViewState.d.ts +143 -1
- package/lib/view/aiCfoView/epics/deleteSkillEpic.d.ts +8 -0
- package/lib/view/aiCfoView/epics/deleteSkillEpic.js +27 -0
- package/lib/view/aiCfoView/epics/fetchSkillDetailEpic.d.ts +8 -0
- package/lib/view/aiCfoView/epics/fetchSkillDetailEpic.js +39 -0
- package/lib/view/aiCfoView/epics/fetchSkillVersionBodyEpic.d.ts +13 -0
- package/lib/view/aiCfoView/epics/fetchSkillVersionBodyEpic.js +49 -0
- package/lib/view/aiCfoView/epics/fetchSkillVersionsEpic.d.ts +8 -0
- package/lib/view/aiCfoView/epics/fetchSkillVersionsEpic.js +41 -0
- package/lib/view/aiCfoView/epics/fetchSkillsEpic.js +11 -6
- package/lib/view/aiCfoView/epics/saveSkillEpic.d.ts +14 -0
- package/lib/view/aiCfoView/epics/saveSkillEpic.js +76 -0
- package/lib/view/aiCfoView/epics/setSkillVisibilityEpic.d.ts +14 -0
- package/lib/view/aiCfoView/epics/setSkillVisibilityEpic.js +36 -0
- package/lib/view/aiCfoView/epics/toSkillDetail.d.ts +88 -0
- package/lib/view/aiCfoView/epics/toSkillDetail.js +110 -0
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +9 -0
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +8 -0
- package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.d.ts +5 -0
- package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +33 -0
- package/lib/view/invoicing/invoicingQboView/epics/invoicingQboViewActionType.d.ts +4 -2
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewPayload.d.ts +0 -26
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewPayload.js +0 -24
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewReducer.d.ts +2 -10
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewReducer.js +4 -41
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +43 -6
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +128 -7
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewState.d.ts +1 -5
- package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
- package/package.json +1 -1
- package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.js +0 -25
- package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.js +0 -15
- package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.d.ts +0 -6
- package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.js +0 -29
- package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.d.ts +0 -6
- package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.js +0 -19
|
@@ -48,8 +48,81 @@ export const initialAiCfoViewState = {
|
|
|
48
48
|
responseStateByChatSessionId: {},
|
|
49
49
|
stopSubmitStateByChatSessionId: {},
|
|
50
50
|
suggestedQuestionsByPageContext: {},
|
|
51
|
-
|
|
51
|
+
skillDetail: {
|
|
52
|
+
fetchState: 'Not-Started',
|
|
53
|
+
error: undefined,
|
|
54
|
+
detail: undefined,
|
|
55
|
+
macroId: undefined,
|
|
56
|
+
},
|
|
57
|
+
skillMutation: {
|
|
58
|
+
fetchState: 'Not-Started',
|
|
59
|
+
error: undefined,
|
|
60
|
+
savedMacroId: undefined,
|
|
61
|
+
},
|
|
62
|
+
skillVersionBody: {
|
|
63
|
+
fetchState: 'Not-Started',
|
|
64
|
+
error: undefined,
|
|
65
|
+
instructions: undefined,
|
|
66
|
+
macroId: undefined,
|
|
67
|
+
version: undefined,
|
|
68
|
+
},
|
|
69
|
+
skillVersions: {
|
|
70
|
+
fetchState: 'Not-Started',
|
|
71
|
+
error: undefined,
|
|
72
|
+
versions: [],
|
|
73
|
+
macroId: undefined,
|
|
74
|
+
},
|
|
75
|
+
skills: {
|
|
76
|
+
fetchState: 'Not-Started',
|
|
77
|
+
error: undefined,
|
|
78
|
+
deletedMacroIds: [],
|
|
79
|
+
skills: [],
|
|
80
|
+
skillsEnabled: false,
|
|
81
|
+
canPublish: false,
|
|
82
|
+
},
|
|
52
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* Forget the skill that was open — prompt AND history together.
|
|
86
|
+
*
|
|
87
|
+
* Both, always. They are two slices describing one selection, so clearing only the
|
|
88
|
+
* prompt leaves `skillVersions.macroId` matching: an in-flight history response then
|
|
89
|
+
* passes its own stale-response guard and renders the previous skill's versions under
|
|
90
|
+
* whatever the user opened next.
|
|
91
|
+
*/
|
|
92
|
+
function clearOpenSkillState(draft) {
|
|
93
|
+
draft.skillDetail = {
|
|
94
|
+
fetchState: 'Not-Started',
|
|
95
|
+
error: undefined,
|
|
96
|
+
detail: undefined,
|
|
97
|
+
macroId: undefined,
|
|
98
|
+
};
|
|
99
|
+
draft.skillVersions = {
|
|
100
|
+
fetchState: 'Not-Started',
|
|
101
|
+
error: undefined,
|
|
102
|
+
versions: [],
|
|
103
|
+
macroId: undefined,
|
|
104
|
+
};
|
|
105
|
+
// Part of the same selection: a pinned body left behind would seed a restore form for
|
|
106
|
+
// a skill the user has left.
|
|
107
|
+
draft.skillVersionBody = {
|
|
108
|
+
fetchState: 'Not-Started',
|
|
109
|
+
error: undefined,
|
|
110
|
+
instructions: undefined,
|
|
111
|
+
macroId: undefined,
|
|
112
|
+
version: undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Take the Skills browser down and drop the prompt it had open.
|
|
117
|
+
*
|
|
118
|
+
* Both halves together, always. Closing without clearing leaves the next open landing on
|
|
119
|
+
* the previous skill instead of the list, and leaves a `macroId` that makes an in-flight
|
|
120
|
+
* detail response look current to the stale-response guard.
|
|
121
|
+
*/
|
|
122
|
+
function closeSkillsBrowserState(draft) {
|
|
123
|
+
draft.uiState.isSkillsBrowserOpen = false;
|
|
124
|
+
clearOpenSkillState(draft);
|
|
125
|
+
}
|
|
53
126
|
const aiCfoView = createSlice({
|
|
54
127
|
name: 'aiCfoView',
|
|
55
128
|
initialState: initialAiCfoViewState,
|
|
@@ -57,6 +130,16 @@ const aiCfoView = createSlice({
|
|
|
57
130
|
submitQuestion: {
|
|
58
131
|
reducer(draft, action) {
|
|
59
132
|
const { sessionId } = action.payload;
|
|
133
|
+
// A turn is being sent, so the chat body has to be what the user sees. Doing this
|
|
134
|
+
// here rather than at each call site is deliberate: the composer, the sample
|
|
135
|
+
// chips, Run, and the `/` menu all submit, and every one of them was a separate
|
|
136
|
+
// chance to forget and leave the answer hidden behind the browser.
|
|
137
|
+
//
|
|
138
|
+
// Guarded so a submit with the browser already closed — i.e. almost every submit
|
|
139
|
+
// — does not produce a new `uiState` for every subscriber to re-render on.
|
|
140
|
+
if (draft.uiState.isSkillsBrowserOpen === true) {
|
|
141
|
+
closeSkillsBrowserState(draft);
|
|
142
|
+
}
|
|
60
143
|
draft.responseStateByChatSessionId[sessionId] = {
|
|
61
144
|
thought: { fetchState: 'In-Progress', error: undefined },
|
|
62
145
|
analysis: { fetchState: 'In-Progress', error: undefined },
|
|
@@ -183,6 +266,13 @@ const aiCfoView = createSlice({
|
|
|
183
266
|
setSession(draft, action) {
|
|
184
267
|
draft.uiState.currentChatSessionId = action.payload.sessionId;
|
|
185
268
|
draft.uiState.agentId = action.payload.agentId;
|
|
269
|
+
// Picking a session means wanting to see that chat, so the browser cannot stay over
|
|
270
|
+
// it. In the reducer because there are six dispatch sites across three separate
|
|
271
|
+
// rails, and covering them one at a time missed half of them.
|
|
272
|
+
if (action.payload.isRestore !== true &&
|
|
273
|
+
draft.uiState.isSkillsBrowserOpen === true) {
|
|
274
|
+
closeSkillsBrowserState(draft);
|
|
275
|
+
}
|
|
186
276
|
mirrorCurrentSessionToPageKeyMapIfActive(draft);
|
|
187
277
|
},
|
|
188
278
|
createSession: {
|
|
@@ -202,6 +292,12 @@ const aiCfoView = createSlice({
|
|
|
202
292
|
draft.uiState.agentId = agentId;
|
|
203
293
|
draft.createSessionState.fetchState = 'Completed';
|
|
204
294
|
draft.createSessionState.error = undefined;
|
|
295
|
+
// Same invariant as submitQuestion and initializeNewSessionState: a new chat exists
|
|
296
|
+
// and is now current, so the browser cannot stay over it. This is the fifth entry
|
|
297
|
+
// point to that rule — the others are submit, new-session, New Chat and setSession.
|
|
298
|
+
if (draft.uiState.isSkillsBrowserOpen === true) {
|
|
299
|
+
closeSkillsBrowserState(draft);
|
|
300
|
+
}
|
|
205
301
|
draft.uiState.currentChatSessionId = sessionId;
|
|
206
302
|
draft.uiState.lastContextMessage = contextMessage;
|
|
207
303
|
draft.uiState.lastContextStatus = contextStatus;
|
|
@@ -243,6 +339,10 @@ const aiCfoView = createSlice({
|
|
|
243
339
|
},
|
|
244
340
|
initializeNewSessionState(draft, action) {
|
|
245
341
|
const { agentId, sessionId } = action.payload;
|
|
342
|
+
// Same invariant as submitQuestion — this is the new-session half of submitting.
|
|
343
|
+
if (draft.uiState.isSkillsBrowserOpen === true) {
|
|
344
|
+
closeSkillsBrowserState(draft);
|
|
345
|
+
}
|
|
246
346
|
draft.uiState.currentChatSessionId = sessionId;
|
|
247
347
|
draft.uiState.agentId = agentId;
|
|
248
348
|
draft.uiState.chatHistoryNextPageToken[sessionId] = {
|
|
@@ -363,6 +463,10 @@ const aiCfoView = createSlice({
|
|
|
363
463
|
};
|
|
364
464
|
draft.allChatSessionIds = draft.allChatSessionIds.filter((id) => id !== chatSessionId);
|
|
365
465
|
if (draft.uiState.currentChatSessionId === chatSessionId) {
|
|
466
|
+
// Deliberately does NOT close the Skills browser, unlike the other writers of
|
|
467
|
+
// this field. Deleting a chat from the history list is housekeeping: if the
|
|
468
|
+
// browser is open the user is not looking at that chat, and closing it would
|
|
469
|
+
// yank them out of what they were doing to reveal an empty chat area.
|
|
366
470
|
draft.uiState.currentChatSessionId = undefined;
|
|
367
471
|
draft.uiState.agentId = undefined;
|
|
368
472
|
}
|
|
@@ -415,6 +519,10 @@ const aiCfoView = createSlice({
|
|
|
415
519
|
},
|
|
416
520
|
},
|
|
417
521
|
clearCurrentSessionId(draft) {
|
|
522
|
+
// New Chat: the empty composer is the point, so the browser cannot stay over it.
|
|
523
|
+
if (draft.uiState.isSkillsBrowserOpen === true) {
|
|
524
|
+
closeSkillsBrowserState(draft);
|
|
525
|
+
}
|
|
418
526
|
draft.uiState.currentChatSessionId = undefined;
|
|
419
527
|
const key = draft.uiState.aiCfoSidePanelHostPageKey;
|
|
420
528
|
if (key != null) {
|
|
@@ -431,8 +539,24 @@ const aiCfoView = createSlice({
|
|
|
431
539
|
draft.uiState.chatSessionIdByPageKey[prevKey] = curId;
|
|
432
540
|
}
|
|
433
541
|
draft.uiState.aiCfoSidePanelHostPageKey = newAiCfoSidePanelHostPageKey;
|
|
434
|
-
|
|
435
|
-
|
|
542
|
+
const nextId = nextCurrentChatSessionId != null ? nextCurrentChatSessionId : undefined;
|
|
543
|
+
// The sixth entry point to the same invariant as submitQuestion,
|
|
544
|
+
// initializeNewSessionState, clearCurrentSessionId, setSession and
|
|
545
|
+
// createSessionSuccess: a different chat is now visible, so the browser cannot stay
|
|
546
|
+
// over it. Missing here, this path swapped the session underneath an open browser
|
|
547
|
+
// and left its macroId looking current to the stale-response guards.
|
|
548
|
+
//
|
|
549
|
+
// Only when the session actually CHANGES, and only when there WAS one. A host page
|
|
550
|
+
// resolving back to the same chat has not changed what is on screen. And
|
|
551
|
+
// `undefined` becoming a saved id is a hydrate, not a navigation — the same
|
|
552
|
+
// settle/restore race `setSession` exempts via `isRestore`: opening Skills on a
|
|
553
|
+
// host page whose chat has not resolved yet would otherwise close the browser
|
|
554
|
+
// under the user the moment the sessions list arrives.
|
|
555
|
+
const hadASession = draft.uiState.currentChatSessionId != null;
|
|
556
|
+
if (hadASession && nextId !== draft.uiState.currentChatSessionId) {
|
|
557
|
+
closeSkillsBrowserState(draft);
|
|
558
|
+
}
|
|
559
|
+
draft.uiState.currentChatSessionId = nextId;
|
|
436
560
|
draft.uiState.currentInput = '';
|
|
437
561
|
mirrorCurrentSessionToPageKeyMapIfActive(draft);
|
|
438
562
|
},
|
|
@@ -477,28 +601,306 @@ const aiCfoView = createSlice({
|
|
|
477
601
|
};
|
|
478
602
|
},
|
|
479
603
|
fetchSkills(draft) {
|
|
480
|
-
//
|
|
481
|
-
//
|
|
604
|
+
// Read straight off `draft.skills` — no optional chain. This store is never
|
|
605
|
+
// persisted or rehydrated (no redux-persist anywhere in the repo, and no
|
|
606
|
+
// `preloadedState` in any consumer), and `initialAiCfoViewState` gives every skills
|
|
607
|
+
// slice a concrete value, so an undefined slice is unreachable here rather than
|
|
608
|
+
// merely unlikely. An earlier version of this comment claimed the opposite and
|
|
609
|
+
// justified a guard by a mechanism that does not exist — which then got applied to
|
|
610
|
+
// some accesses and not others, so the file read as if the slice were sometimes
|
|
611
|
+
// missing and sometimes not. The one guard that IS meaningful lives in the
|
|
612
|
+
// selectors, where `state.aiCfoViewState` may genuinely be absent for a host that
|
|
613
|
+
// composes a partial root reducer; that is a boundary, and this is not.
|
|
482
614
|
draft.skills = {
|
|
483
615
|
fetchState: 'In-Progress',
|
|
484
616
|
error: undefined,
|
|
485
|
-
skills: draft.skills
|
|
617
|
+
skills: draft.skills.skills,
|
|
618
|
+
skillsEnabled: draft.skills.skillsEnabled,
|
|
619
|
+
// Carried for the same reason as skillsEnabled: a refetch in flight is not a
|
|
620
|
+
// reason to hide the publish control the user was just looking at.
|
|
621
|
+
canPublish: draft.skills.canPublish,
|
|
622
|
+
// Carried, not cleared: the response this is about to fetch may still contain a
|
|
623
|
+
// skill deleted moments ago, which is the whole reason the list is kept.
|
|
624
|
+
deletedMacroIds: draft.skills.deletedMacroIds,
|
|
486
625
|
};
|
|
487
626
|
},
|
|
488
627
|
fetchSkillsSuccess(draft, action) {
|
|
628
|
+
// A list response can predate a delete — a save emits fetchSkills, and a delete can
|
|
629
|
+
// land while that is still outstanding — so replacing the array wholesale would put
|
|
630
|
+
// the deleted row back and read as the delete having failed.
|
|
489
631
|
draft.skills = {
|
|
490
632
|
fetchState: 'Completed',
|
|
491
633
|
error: undefined,
|
|
492
|
-
skills: action.payload.skills,
|
|
634
|
+
skills: action.payload.skills.filter((skill) => !draft.skills.deletedMacroIds.includes(skill.macroId)),
|
|
635
|
+
skillsEnabled: action.payload.skillsEnabled,
|
|
636
|
+
canPublish: action.payload.canPublish,
|
|
637
|
+
// Applied ONCE, then forgotten — the tombstone covers a race window, not a
|
|
638
|
+
// permanent state.
|
|
639
|
+
//
|
|
640
|
+
// The previous rule (keep it until a response omits that skill) was wrong in the
|
|
641
|
+
// case it was written for: nothing refetches after a delete, and a create emits
|
|
642
|
+
// fetchSkills whose response DOES contain the id, so the tombstone was kept and
|
|
643
|
+
// reapplied forever. A skill recreated under the same id stayed invisible with no
|
|
644
|
+
// way to clear it.
|
|
645
|
+
deletedMacroIds: [],
|
|
493
646
|
};
|
|
494
647
|
},
|
|
495
648
|
fetchSkillsFailure(draft, action) {
|
|
496
649
|
draft.skills = {
|
|
497
650
|
fetchState: 'Error',
|
|
498
651
|
error: action.payload,
|
|
499
|
-
skills: draft.skills
|
|
652
|
+
skills: draft.skills.skills,
|
|
653
|
+
// Kept, not reset: a failed refresh is not evidence the feature was turned
|
|
654
|
+
// off, and dropping it to false would make the Skills entry vanish on any
|
|
655
|
+
// transient error.
|
|
656
|
+
skillsEnabled: draft.skills.skillsEnabled,
|
|
657
|
+
// Same reasoning again: a transient error must not revoke a privilege the server
|
|
658
|
+
// already granted, or the control flickers away on every failed refresh.
|
|
659
|
+
canPublish: draft.skills.canPublish,
|
|
660
|
+
// Same reasoning: a failed refresh is not confirmation the delete was undone.
|
|
661
|
+
deletedMacroIds: draft.skills.deletedMacroIds,
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
fetchSkillDetail(draft, action) {
|
|
665
|
+
// Opening a skill IS the selection change, so forget the previous one whole —
|
|
666
|
+
// history included. Resetting only the prompt left `skillVersions.macroId` pointing
|
|
667
|
+
// at the skill just navigated away from, so a history response still in flight for
|
|
668
|
+
// it passed its own guard and rendered under the new title. Close, back and delete
|
|
669
|
+
// all go through the same helper for the same reason.
|
|
670
|
+
clearOpenSkillState(draft);
|
|
671
|
+
draft.skillDetail = {
|
|
672
|
+
fetchState: 'In-Progress',
|
|
673
|
+
error: undefined,
|
|
674
|
+
// Cleared rather than kept: holding the previous skill's prompt while a new
|
|
675
|
+
// one loads would show the wrong instructions under the right title.
|
|
676
|
+
detail: undefined,
|
|
677
|
+
macroId: action.payload.macroId,
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
fetchSkillDetailSuccess(draft, action) {
|
|
681
|
+
// Ignore a response for a skill the user has already navigated away from.
|
|
682
|
+
if (draft.skillDetail.macroId !== action.payload.macroId) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
draft.skillDetail = {
|
|
686
|
+
fetchState: 'Completed',
|
|
687
|
+
error: undefined,
|
|
688
|
+
detail: action.payload.detail,
|
|
689
|
+
macroId: action.payload.macroId,
|
|
690
|
+
};
|
|
691
|
+
},
|
|
692
|
+
fetchSkillDetailFailure(draft, action) {
|
|
693
|
+
if (draft.skillDetail.macroId !== action.payload.macroId) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
draft.skillDetail = {
|
|
697
|
+
fetchState: 'Error',
|
|
698
|
+
error: action.payload.error,
|
|
699
|
+
detail: undefined,
|
|
700
|
+
macroId: action.payload.macroId,
|
|
500
701
|
};
|
|
501
702
|
},
|
|
703
|
+
clearSkillDetail(draft) {
|
|
704
|
+
// Back to the list. History belongs to the detail view, so it goes too.
|
|
705
|
+
clearOpenSkillState(draft);
|
|
706
|
+
},
|
|
707
|
+
fetchSkillVersions: {
|
|
708
|
+
reducer(draft, action) {
|
|
709
|
+
draft.skillVersions = {
|
|
710
|
+
fetchState: 'In-Progress',
|
|
711
|
+
error: undefined,
|
|
712
|
+
// Cleared, not kept: showing the previous skill's history under a new title
|
|
713
|
+
// is worse than showing none while this loads.
|
|
714
|
+
versions: [],
|
|
715
|
+
macroId: action.payload.macroId,
|
|
716
|
+
};
|
|
717
|
+
},
|
|
718
|
+
prepare(payload) {
|
|
719
|
+
return { payload };
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
fetchSkillVersionsSuccess(draft, action) {
|
|
723
|
+
// Same stale-response guard as skillDetail: a switchMap cancels the request but
|
|
724
|
+
// cannot un-dispatch one already in flight.
|
|
725
|
+
if (draft.skillVersions.macroId !== action.payload.macroId) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
draft.skillVersions.fetchState = 'Completed';
|
|
729
|
+
draft.skillVersions.error = undefined;
|
|
730
|
+
draft.skillVersions.versions = action.payload.versions;
|
|
731
|
+
},
|
|
732
|
+
fetchSkillVersionsFailure(draft, action) {
|
|
733
|
+
if (draft.skillVersions.macroId !== action.payload.macroId) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
draft.skillVersions.fetchState = 'Error';
|
|
737
|
+
draft.skillVersions.error = action.payload.error;
|
|
738
|
+
},
|
|
739
|
+
fetchSkillVersionBody: {
|
|
740
|
+
reducer(draft, action) {
|
|
741
|
+
draft.skillVersionBody = {
|
|
742
|
+
fetchState: 'In-Progress',
|
|
743
|
+
error: undefined,
|
|
744
|
+
// Cleared: seeding a restore form from the PREVIOUS version's body is the same
|
|
745
|
+
// class of mistake as showing one skill's prompt under another's title.
|
|
746
|
+
instructions: undefined,
|
|
747
|
+
macroId: action.payload.macroId,
|
|
748
|
+
version: action.payload.version,
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
prepare(payload) {
|
|
752
|
+
return { payload };
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
fetchSkillVersionBodySuccess(draft, action) {
|
|
756
|
+
// Both parts of the key: a user can jump between versions of the same skill, so
|
|
757
|
+
// macroId alone would let an earlier version's body land on a later request.
|
|
758
|
+
if (draft.skillVersionBody.macroId !== action.payload.macroId ||
|
|
759
|
+
draft.skillVersionBody.version !== action.payload.version) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
draft.skillVersionBody.fetchState = 'Completed';
|
|
763
|
+
draft.skillVersionBody.error = undefined;
|
|
764
|
+
draft.skillVersionBody.instructions = action.payload.instructions;
|
|
765
|
+
},
|
|
766
|
+
fetchSkillVersionBodyFailure(draft, action) {
|
|
767
|
+
if (draft.skillVersionBody.macroId !== action.payload.macroId ||
|
|
768
|
+
draft.skillVersionBody.version !== action.payload.version) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
draft.skillVersionBody.fetchState = 'Error';
|
|
772
|
+
draft.skillVersionBody.error = action.payload.error;
|
|
773
|
+
},
|
|
774
|
+
saveSkill: {
|
|
775
|
+
reducer(draft) {
|
|
776
|
+
draft.skillMutation = {
|
|
777
|
+
fetchState: 'In-Progress',
|
|
778
|
+
error: undefined,
|
|
779
|
+
savedMacroId: undefined,
|
|
780
|
+
};
|
|
781
|
+
},
|
|
782
|
+
prepare(payload) {
|
|
783
|
+
return { payload };
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
saveSkillSuccess(draft, action) {
|
|
787
|
+
draft.skillMutation.fetchState = 'Completed';
|
|
788
|
+
draft.skillMutation.error = undefined;
|
|
789
|
+
draft.skillMutation.savedMacroId = action.payload.macroId;
|
|
790
|
+
// ONLY when the saved skill is still the one on screen. The save is a mergeMap —
|
|
791
|
+
// deliberately not cancellable, because cancelling a write only hides its outcome —
|
|
792
|
+
// so it can land after the user opened another skill, went back to the list, closed
|
|
793
|
+
// the browser, or deleted it. Writing unconditionally would drop one skill's prompt
|
|
794
|
+
// on top of another's, which is precisely what the fetch guards exist to prevent.
|
|
795
|
+
//
|
|
796
|
+
// A create lands here with nothing open, and is deliberately NOT force-opened:
|
|
797
|
+
// `savedMacroId` is the signal for "what did I just create", and the caller decides
|
|
798
|
+
// whether to open it.
|
|
799
|
+
if (draft.skillDetail.macroId !== action.payload.macroId) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
// The save response IS a detail read, so adopt it. Discarding it left an editor
|
|
803
|
+
// sitting on the same skill showing the prompt and card it had BEFORE the save —
|
|
804
|
+
// the user's own change missing from the thing they just saved.
|
|
805
|
+
draft.skillDetail = {
|
|
806
|
+
fetchState: 'Completed',
|
|
807
|
+
error: undefined,
|
|
808
|
+
detail: action.payload.detail,
|
|
809
|
+
macroId: action.payload.macroId,
|
|
810
|
+
};
|
|
811
|
+
// History is now a version behind; the epic refetches it.
|
|
812
|
+
draft.skillVersions = {
|
|
813
|
+
fetchState: 'Not-Started',
|
|
814
|
+
error: undefined,
|
|
815
|
+
versions: [],
|
|
816
|
+
macroId: undefined,
|
|
817
|
+
};
|
|
818
|
+
},
|
|
819
|
+
saveSkillFailure(draft, action) {
|
|
820
|
+
draft.skillMutation.fetchState = 'Error';
|
|
821
|
+
draft.skillMutation.error = action.payload;
|
|
822
|
+
},
|
|
823
|
+
setSkillVisibility: {
|
|
824
|
+
reducer(draft) {
|
|
825
|
+
// Shares skillMutation with save and delete. One in-flight write at a time is the
|
|
826
|
+
// existing contract here, and publishing is a write like any other.
|
|
827
|
+
draft.skillMutation = {
|
|
828
|
+
fetchState: 'In-Progress',
|
|
829
|
+
error: undefined,
|
|
830
|
+
savedMacroId: undefined,
|
|
831
|
+
};
|
|
832
|
+
},
|
|
833
|
+
prepare(payload) {
|
|
834
|
+
return { payload };
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
setSkillVisibilitySuccess(draft, action) {
|
|
838
|
+
draft.skillMutation.fetchState = 'Completed';
|
|
839
|
+
draft.skillMutation.error = undefined;
|
|
840
|
+
draft.skillMutation.savedMacroId = action.payload.macroId;
|
|
841
|
+
// Same staleness guard as saveSkillSuccess, for the same reason: this is a mergeMap
|
|
842
|
+
// write, so it can land after the user opened another skill.
|
|
843
|
+
if (draft.skillDetail.macroId !== action.payload.macroId) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
draft.skillDetail = {
|
|
847
|
+
fetchState: 'Completed',
|
|
848
|
+
error: undefined,
|
|
849
|
+
detail: action.payload.detail,
|
|
850
|
+
macroId: action.payload.macroId,
|
|
851
|
+
};
|
|
852
|
+
// Deliberately NOT clearing skillVersions, unlike saveSkillSuccess. No version was
|
|
853
|
+
// created, so the history the user is looking at is still correct — refetching it
|
|
854
|
+
// would flash a loading state for a list that did not change.
|
|
855
|
+
},
|
|
856
|
+
setSkillVisibilityFailure(draft, action) {
|
|
857
|
+
draft.skillMutation.fetchState = 'Error';
|
|
858
|
+
draft.skillMutation.error = action.payload;
|
|
859
|
+
},
|
|
860
|
+
deleteSkill: {
|
|
861
|
+
reducer(draft) {
|
|
862
|
+
draft.skillMutation = {
|
|
863
|
+
fetchState: 'In-Progress',
|
|
864
|
+
error: undefined,
|
|
865
|
+
savedMacroId: undefined,
|
|
866
|
+
};
|
|
867
|
+
},
|
|
868
|
+
prepare(payload) {
|
|
869
|
+
return { payload };
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
deleteSkillSuccess(draft, action) {
|
|
873
|
+
draft.skillMutation.fetchState = 'Completed';
|
|
874
|
+
draft.skillMutation.error = undefined;
|
|
875
|
+
// Drop it from the list immediately rather than waiting for a refetch: the row
|
|
876
|
+
// the user just deleted staying on screen reads as the delete having failed.
|
|
877
|
+
draft.skills.skills = draft.skills.skills.filter((skill) => skill.macroId !== action.payload.macroId);
|
|
878
|
+
if (!draft.skills.deletedMacroIds.includes(action.payload.macroId)) {
|
|
879
|
+
draft.skills.deletedMacroIds.push(action.payload.macroId);
|
|
880
|
+
}
|
|
881
|
+
if (draft.skillDetail.macroId === action.payload.macroId) {
|
|
882
|
+
clearOpenSkillState(draft);
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
deleteSkillFailure(draft, action) {
|
|
886
|
+
draft.skillMutation.fetchState = 'Error';
|
|
887
|
+
draft.skillMutation.error = action.payload;
|
|
888
|
+
},
|
|
889
|
+
clearSkillMutation(draft) {
|
|
890
|
+
draft.skillMutation = {
|
|
891
|
+
fetchState: 'Not-Started',
|
|
892
|
+
error: undefined,
|
|
893
|
+
savedMacroId: undefined,
|
|
894
|
+
};
|
|
895
|
+
},
|
|
896
|
+
openSkillsBrowser(draft) {
|
|
897
|
+
draft.uiState.isSkillsBrowserOpen = true;
|
|
898
|
+
},
|
|
899
|
+
closeSkillsBrowser(draft) {
|
|
900
|
+
// Unguarded, unlike the implicit closes: an explicit close should clear a lingering
|
|
901
|
+
// detail even if the flag is already false.
|
|
902
|
+
closeSkillsBrowserState(draft);
|
|
903
|
+
},
|
|
502
904
|
updateCotCollapsedState(draft, action) {
|
|
503
905
|
const { isCollapsed, questionAnswerId } = action.payload;
|
|
504
906
|
draft.uiState.cotCollapsedByQuestionAnswerId[questionAnswerId] =
|
|
@@ -511,5 +913,5 @@ const aiCfoView = createSlice({
|
|
|
511
913
|
});
|
|
512
914
|
},
|
|
513
915
|
});
|
|
514
|
-
export const { submitQuestion, submitQuestionSuccess, submitQuestionFailure, updateResponseState, updateAiCfoViewScrollPosition, updateCurrentInput, submitFeedback, submitFeedbackSuccess, submitFeedbackFailure, setSession, createSession, createSessionSuccess, createSessionFailure, createSessionAndSubmit, initializeNewSessionState, createSessionAndSubmitSuccess, createSessionAndSubmitFailure, fetchChatSessionsForUser, fetchChatSessionsForUserSuccess, fetchChatSessionsForUserFailure, fetchChatHistory, fetchChatHistorySuccess, fetchChatHistoryFailure, clearCurrentSessionId, clearLastContextMessage, stopSubmit, stopSubmitSuccess, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionSuccess, stopSubmitQuestionFailure, acceptMasterTOS, acceptMasterTOSSuccess, acceptMasterTOSFailure, deleteChatSession, deleteChatSessionSuccess, deleteChatSessionFailure, clearDeleteChatSessionStatus, updateCotCollapsedState, fetchSuggestedQuestions, fetchSuggestedQuestionsSuccess, fetchSuggestedQuestionsFailure, fetchSkills, fetchSkillsSuccess, fetchSkillsFailure, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, } = aiCfoView.actions;
|
|
916
|
+
export const { submitQuestion, submitQuestionSuccess, submitQuestionFailure, updateResponseState, updateAiCfoViewScrollPosition, updateCurrentInput, submitFeedback, submitFeedbackSuccess, submitFeedbackFailure, setSession, createSession, createSessionSuccess, createSessionFailure, createSessionAndSubmit, initializeNewSessionState, createSessionAndSubmitSuccess, createSessionAndSubmitFailure, fetchChatSessionsForUser, fetchChatSessionsForUserSuccess, fetchChatSessionsForUserFailure, fetchChatHistory, fetchChatHistorySuccess, fetchChatHistoryFailure, clearCurrentSessionId, clearLastContextMessage, stopSubmit, stopSubmitSuccess, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionSuccess, stopSubmitQuestionFailure, acceptMasterTOS, acceptMasterTOSSuccess, acceptMasterTOSFailure, deleteChatSession, deleteChatSessionSuccess, deleteChatSessionFailure, clearDeleteChatSessionStatus, updateCotCollapsedState, fetchSuggestedQuestions, fetchSuggestedQuestionsSuccess, fetchSuggestedQuestionsFailure, fetchSkills, fetchSkillsSuccess, fetchSkillsFailure, fetchSkillDetail, fetchSkillDetailSuccess, fetchSkillDetailFailure, clearSkillDetail, clearSkillMutation, deleteSkill, deleteSkillSuccess, deleteSkillFailure, fetchSkillVersionBody, fetchSkillVersionBodySuccess, fetchSkillVersionBodyFailure, fetchSkillVersions, fetchSkillVersionsSuccess, fetchSkillVersionsFailure, saveSkill, saveSkillSuccess, saveSkillFailure, setSkillVisibility, setSkillVisibilitySuccess, setSkillVisibilityFailure, openSkillsBrowser, closeSkillsBrowser, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, } = aiCfoView.actions;
|
|
515
917
|
export default aiCfoView.reducer;
|
|
@@ -29,7 +29,56 @@ const EMPTY_SKILLS_STATE = {
|
|
|
29
29
|
fetchState: 'Not-Started',
|
|
30
30
|
error: undefined,
|
|
31
31
|
skills: [],
|
|
32
|
+
// false, so the Skills entry stays hidden until the server confirms the feature
|
|
33
|
+
// is on. Defaulting true would flash a menu at tenants who never get one.
|
|
34
|
+
skillsEnabled: false,
|
|
35
|
+
// false, for the same reason as skillsEnabled: withhold the publish control until the
|
|
36
|
+
// server says this user may use it.
|
|
37
|
+
canPublish: false,
|
|
38
|
+
deletedMacroIds: [],
|
|
39
|
+
};
|
|
40
|
+
const EMPTY_SKILL_DETAIL_STATE = {
|
|
41
|
+
fetchState: 'Not-Started',
|
|
42
|
+
error: undefined,
|
|
43
|
+
detail: undefined,
|
|
44
|
+
macroId: undefined,
|
|
45
|
+
};
|
|
46
|
+
// Same stable-fallback reason as the two above: a selector that built a fresh object
|
|
47
|
+
// each call would re-render every subscriber on every unrelated state change.
|
|
48
|
+
const EMPTY_SKILL_VERSIONS_STATE = {
|
|
49
|
+
fetchState: 'Not-Started',
|
|
50
|
+
error: undefined,
|
|
51
|
+
versions: [],
|
|
52
|
+
macroId: undefined,
|
|
53
|
+
};
|
|
54
|
+
const EMPTY_SKILL_VERSION_BODY_STATE = {
|
|
55
|
+
fetchState: 'Not-Started',
|
|
56
|
+
error: undefined,
|
|
57
|
+
instructions: undefined,
|
|
58
|
+
macroId: undefined,
|
|
59
|
+
version: undefined,
|
|
60
|
+
};
|
|
61
|
+
const EMPTY_SKILL_MUTATION_STATE = {
|
|
62
|
+
fetchState: 'Not-Started',
|
|
63
|
+
error: undefined,
|
|
64
|
+
savedMacroId: undefined,
|
|
32
65
|
};
|
|
33
66
|
export function getSkills(state) {
|
|
34
67
|
return state.aiCfoViewState?.skills ?? EMPTY_SKILLS_STATE;
|
|
35
68
|
}
|
|
69
|
+
export function getSkillDetail(state) {
|
|
70
|
+
return state.aiCfoViewState?.skillDetail ?? EMPTY_SKILL_DETAIL_STATE;
|
|
71
|
+
}
|
|
72
|
+
export function getSkillVersions(state) {
|
|
73
|
+
return state.aiCfoViewState?.skillVersions ?? EMPTY_SKILL_VERSIONS_STATE;
|
|
74
|
+
}
|
|
75
|
+
export function getSkillVersionBody(state) {
|
|
76
|
+
return (state.aiCfoViewState?.skillVersionBody ?? EMPTY_SKILL_VERSION_BODY_STATE);
|
|
77
|
+
}
|
|
78
|
+
export function getSkillMutation(state) {
|
|
79
|
+
return state.aiCfoViewState?.skillMutation ?? EMPTY_SKILL_MUTATION_STATE;
|
|
80
|
+
}
|
|
81
|
+
/** True only when the server says the feature is on for this tenant. */
|
|
82
|
+
export function getSkillsEnabled(state) {
|
|
83
|
+
return getSkills(state).skillsEnabled;
|
|
84
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap } from 'rxjs/operators';
|
|
3
|
+
import { createZeniAPIStatus, isSuccessStatus } from '../../../responsePayload';
|
|
4
|
+
import { deleteSkill, deleteSkillFailure, deleteSkillSuccess, } from '../aiCfoViewReducer';
|
|
5
|
+
/** DELETE /1.0/skills/{macroId} — soft-deletes every version, frees the name. */
|
|
6
|
+
export const deleteSkillEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(deleteSkill.match),
|
|
7
|
+
// mergeMap for the same reason as the save: cancelling a delete does not un-delete
|
|
8
|
+
// it, and two deletes are for different skills anyway.
|
|
9
|
+
mergeMap((action) => {
|
|
10
|
+
const { macroId } = action.payload;
|
|
11
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}`;
|
|
12
|
+
return zeniAPI.deleteAndGetJSON(url).pipe(mergeMap((response) => {
|
|
13
|
+
// isSuccessStatus, NOT isSuccessResponse: the latter also requires a non-null
|
|
14
|
+
// `data`, and a successful DELETE here is a 204 with no body. Using it reported
|
|
15
|
+
// every completed delete as a failure — the row stayed on screen and a retry
|
|
16
|
+
// 404'd on a skill that was already gone. deleteChatSessionEpic uses the status
|
|
17
|
+
// check for the same reason.
|
|
18
|
+
if (isSuccessStatus(response)) {
|
|
19
|
+
return of(deleteSkillSuccess({ macroId }));
|
|
20
|
+
}
|
|
21
|
+
return of(deleteSkillFailure(response.status));
|
|
22
|
+
}), catchError((error) => of(deleteSkillFailure(createZeniAPIStatus('Unexpected Error', 'Delete skill REST API call failed', error instanceof Error ? { message: error.message } : undefined)))));
|
|
23
|
+
}));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
|
|
3
|
+
import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
|
|
4
|
+
import { fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, } from '../aiCfoViewReducer';
|
|
5
|
+
import { isSkillDetailPayload, toSkillDetail } from './toSkillDetail';
|
|
6
|
+
// Deliberately minimal: the browser already holds the full Skill from the list, and
|
|
7
|
+
// this response's `skill` block exists so a deep link can open a detail view without
|
|
8
|
+
// the list ever having been fetched.
|
|
9
|
+
/** GET {aiCfoMicroServiceBaseUrl}/1.0/skills/{macroId} — one skill's prompt + card. */
|
|
10
|
+
export const fetchSkillDetailEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchSkillDetail.match),
|
|
11
|
+
// switchMap: clicking through the list fires one of these per skill, and only the
|
|
12
|
+
// one the user is currently looking at matters. The reducer also drops responses
|
|
13
|
+
// whose macroId no longer matches, so a cancelled-but-in-flight request cannot
|
|
14
|
+
// render the wrong prompt under the right title.
|
|
15
|
+
switchMap((action) => {
|
|
16
|
+
const { macroId } = action.payload;
|
|
17
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}`;
|
|
18
|
+
return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
|
|
19
|
+
// Shared mapper and shared guard. This epic used to keep its own `toSkill`
|
|
20
|
+
// and default a missing prompt to '', so the same malformed body showed an
|
|
21
|
+
// empty prompt here and failed the read elsewhere — and `isMine` was nearly
|
|
22
|
+
// added to only two of the three copies.
|
|
23
|
+
if (isSuccessResponse(response) &&
|
|
24
|
+
isSkillDetailPayload(response.data)) {
|
|
25
|
+
return of(fetchSkillDetailSuccess({
|
|
26
|
+
detail: toSkillDetail(response.data),
|
|
27
|
+
macroId,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
return of(fetchSkillDetailFailure({ error: response.status, macroId }));
|
|
31
|
+
}), catchError((error) => of(fetchSkillDetailFailure({
|
|
32
|
+
error: createZeniAPIStatus('Unexpected Error', 'Fetch skill detail REST API call failed', error instanceof Error ? { message: error.message } : undefined),
|
|
33
|
+
macroId,
|
|
34
|
+
}))));
|
|
35
|
+
}));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
|
|
3
|
+
import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
|
|
4
|
+
import { fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, } from '../aiCfoViewReducer';
|
|
5
|
+
import { hasPromptBody } from './toSkillDetail';
|
|
6
|
+
/**
|
|
7
|
+
* GET /1.0/skills/{macroId}?version=N — one historical version's prompt.
|
|
8
|
+
*
|
|
9
|
+
* Only used to seed a restore. The history list carries metadata only, deliberately: a
|
|
10
|
+
* 20-version history would otherwise be a 240kB response for a panel that shows dates.
|
|
11
|
+
*/
|
|
12
|
+
export const fetchSkillVersionBodyEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchSkillVersionBody.match),
|
|
13
|
+
// switchMap: a read, and only the version the user last clicked matters. The reducer
|
|
14
|
+
// also drops a response whose (macroId, version) no longer matches, so a
|
|
15
|
+
// cancelled-but-in-flight request cannot seed the form with the wrong body.
|
|
16
|
+
switchMap((action) => {
|
|
17
|
+
const { macroId, version } = action.payload;
|
|
18
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}?version=${encodeURIComponent(String(version))}`;
|
|
19
|
+
return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
|
|
20
|
+
// `instructions` checked, not just `data`: the payload TYPE says the field is
|
|
21
|
+
// required, and the network is under no obligation to agree. Without this the
|
|
22
|
+
// epic reported success with `instructions: undefined`, and the browser — which
|
|
23
|
+
// holds the editor closed until a body arrives — showed "Loading this version…"
|
|
24
|
+
// for ever, with no error and no way forward.
|
|
25
|
+
if (isSuccessResponse(response) && hasPromptBody(response.data)) {
|
|
26
|
+
return of(fetchSkillVersionBodySuccess({
|
|
27
|
+
macroId,
|
|
28
|
+
version,
|
|
29
|
+
instructions: response.data.instructions,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
// A malformed 2xx is a failure. An empty STRING is not malformed and is allowed
|
|
33
|
+
// through: the editor's own minimum-length rule refuses to save it, so the user
|
|
34
|
+
// sees a validation message on a form they can fix rather than a dead end.
|
|
35
|
+
return of(fetchSkillVersionBodyFailure({
|
|
36
|
+
macroId,
|
|
37
|
+
version,
|
|
38
|
+
error: response.status,
|
|
39
|
+
}));
|
|
40
|
+
}), catchError((error) => of(fetchSkillVersionBodyFailure({
|
|
41
|
+
macroId,
|
|
42
|
+
version,
|
|
43
|
+
error: createZeniAPIStatus('Unexpected Error', 'Fetch skill version body REST API call failed', error instanceof Error ? { message: error.message } : undefined),
|
|
44
|
+
}))));
|
|
45
|
+
}));
|