@zeniai/client-epic-state 5.2.4 → 5.2.5
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 +12 -0
- package/lib/esm/epic.js +12 -0
- package/lib/esm/index.js +2 -2
- 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/index.d.ts +3 -3
- package/lib/index.js +26 -11
- 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/taskManager/taskListView/taskList.d.ts +1 -1
- package/package.json +1 -1
|
@@ -29,6 +29,15 @@ export interface AiCfoViewUIState {
|
|
|
29
29
|
*/
|
|
30
30
|
aiCfoSidePanelHostPageKey?: string;
|
|
31
31
|
currentChatSessionId?: ID;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the AI CFO page body shows the Skills browser instead of the chat.
|
|
34
|
+
*
|
|
35
|
+
* In Redux rather than component state because the entry that opens it lives in the
|
|
36
|
+
* app drawer's rail (`AiCfoMenuContent`, reached via both `AppDrawer` on desktop and
|
|
37
|
+
* the sidebar drawer on mobile) while the browser renders in `AiCfoPage` — sibling
|
|
38
|
+
* subtrees with no common owner below the store.
|
|
39
|
+
*/
|
|
40
|
+
isSkillsBrowserOpen?: boolean;
|
|
32
41
|
lastContextMessage?: string;
|
|
33
42
|
lastContextStatus?: ContextStatus;
|
|
34
43
|
}
|
|
@@ -47,12 +56,137 @@ export interface Skill {
|
|
|
47
56
|
*/
|
|
48
57
|
exampleHints: string[];
|
|
49
58
|
hasScripts: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the signed-in user authored this skill. Distinct from `isOwnedByTenant`, and
|
|
61
|
+
* the stricter of the two: writes require ownership, so a colleague's shared skill is
|
|
62
|
+
* `isOwnedByTenant: true` and `isMine: false`. False for a Zeni default.
|
|
63
|
+
*/
|
|
64
|
+
isMine: boolean;
|
|
65
|
+
/** False for a Zeni default, true for a skill this tenant owns. */
|
|
66
|
+
isOwnedByTenant: boolean;
|
|
50
67
|
macroId: string;
|
|
51
68
|
name: string;
|
|
52
69
|
semver: string | null;
|
|
70
|
+
/** ISO timestamp of the latest version, for the list's "last updated" column. */
|
|
71
|
+
updatedAt: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* 'private' (author-only) or 'tenant' (everyone). Null for Zeni defaults, where it does
|
|
74
|
+
* not apply — and also null for a value this client does not recognise, which is
|
|
75
|
+
* deliberate: consumers render no audience badge rather than a wrong one, and adding a
|
|
76
|
+
* third tier server-side then surfaces here as a code change instead of silently
|
|
77
|
+
* matching none of the branches.
|
|
78
|
+
*
|
|
79
|
+
* The same union as {@link SkillDraft.visibility}. It was `string | null`, which left
|
|
80
|
+
* every consumer comparing against bare literals with no compile-time backing on the
|
|
81
|
+
* READ path while the write path had it.
|
|
82
|
+
*/
|
|
83
|
+
visibility: SkillVisibility | null;
|
|
84
|
+
}
|
|
85
|
+
/** One skill's prompt plus the rendered card, from GET /1.0/skills/<macroId>. */
|
|
86
|
+
export interface SkillDetail {
|
|
87
|
+
artifact: string;
|
|
88
|
+
artifactTitle: string;
|
|
89
|
+
instructions: string;
|
|
90
|
+
instructionsCharCount: number;
|
|
91
|
+
skill: Skill;
|
|
53
92
|
}
|
|
54
93
|
export interface SkillsState extends FetchStateAndError {
|
|
94
|
+
/**
|
|
95
|
+
* Whether the signed-in user may publish a skill tenant-wide. From the server, which
|
|
96
|
+
* is the only place that knows: it is a scope granted to admin roles. Starts false so
|
|
97
|
+
* the control is withheld until the server grants it.
|
|
98
|
+
*/
|
|
99
|
+
canPublish: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Skills deleted locally, so a list response that was already in flight cannot put
|
|
102
|
+
* them back.
|
|
103
|
+
*
|
|
104
|
+
* The delete and the refetch race by construction — a save emits `fetchSkills`, and a
|
|
105
|
+
* delete can land while that is outstanding — and `fetchSkillsSuccess` replaces the
|
|
106
|
+
* array wholesale. Without this the row reappears, which reads as the delete having
|
|
107
|
+
* failed. Entries are dropped once a response arrives that no longer contains them,
|
|
108
|
+
* so this cannot grow without bound or outlive the server agreeing.
|
|
109
|
+
*/
|
|
110
|
+
deletedMacroIds: string[];
|
|
55
111
|
skills: Skill[];
|
|
112
|
+
/**
|
|
113
|
+
* Whether the feature is on for this tenant. Starts false so the Skills entry
|
|
114
|
+
* stays hidden until the server says otherwise — an empty `skills` list is not
|
|
115
|
+
* evidence either way.
|
|
116
|
+
*/
|
|
117
|
+
skillsEnabled: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* What the editor submits. Mirrors the server's write model, which is deliberately
|
|
121
|
+
* narrower than a skill: `scripts`, `tenantId`, `macroId` and menu-visibility are all
|
|
122
|
+
* rejected there, so they have no place here either.
|
|
123
|
+
*/
|
|
124
|
+
/** Who can see a skill. 'private' is its author only, 'tenant' is everyone. */
|
|
125
|
+
export type SkillVisibility = 'private' | 'tenant';
|
|
126
|
+
export interface SkillDraft {
|
|
127
|
+
/** One line under the name in the menu. What makes a list browsable. */
|
|
128
|
+
description: string | null;
|
|
129
|
+
/** The prompt. Multi-paragraph — this is the skill, functionally. */
|
|
130
|
+
instructions: string;
|
|
131
|
+
/** The `/` command and the menu row's title. */
|
|
132
|
+
name: string;
|
|
133
|
+
/**
|
|
134
|
+
* The audience to SET, or null to leave it exactly as it is.
|
|
135
|
+
*
|
|
136
|
+
* Required, and null is a real choice rather than an oversight, because the two are
|
|
137
|
+
* not interchangeable on the wire: the server treats an omitted visibility as
|
|
138
|
+
* "unchanged" and an explicit 'private' as "make this private". A caller who cannot
|
|
139
|
+
* publish must therefore send null — sending 'private' would unpublish a tenant-wide
|
|
140
|
+
* skill on every unrelated edit, silently and with a 200.
|
|
141
|
+
*/
|
|
142
|
+
visibility: SkillVisibility | null;
|
|
143
|
+
}
|
|
144
|
+
/** One row of a skill's history. Metadata only — bodies are read per version. */
|
|
145
|
+
export interface SkillVersion {
|
|
146
|
+
/** Menu name AT this version, so a rename is visible in the list. */
|
|
147
|
+
displayName: string | null;
|
|
148
|
+
instructionsCharCount: number;
|
|
149
|
+
isActive: boolean;
|
|
150
|
+
/** Whether this is the version the `/` menu runs. */
|
|
151
|
+
isLatest: boolean;
|
|
152
|
+
semver: string | null;
|
|
153
|
+
updatedAt: string | null;
|
|
154
|
+
version: number;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One historical version's prompt, fetched on demand for a restore.
|
|
158
|
+
*
|
|
159
|
+
* Separate from `skillDetail`, which holds the LIVE version: a restore needs both at once
|
|
160
|
+
* — the old body to seed the form, the current skill for its name and id — so writing the
|
|
161
|
+
* pinned body into `skillDetail` would lose the thing being restored onto.
|
|
162
|
+
*/
|
|
163
|
+
export interface SkillVersionBodyState extends FetchStateAndError {
|
|
164
|
+
instructions: string | undefined;
|
|
165
|
+
/** Which (skill, version) `instructions` belongs to, so a stale response is dropped. */
|
|
166
|
+
macroId: string | undefined;
|
|
167
|
+
version: number | undefined;
|
|
168
|
+
}
|
|
169
|
+
export interface SkillVersionsState extends FetchStateAndError {
|
|
170
|
+
/** Which skill `versions` belongs to, so a stale response is never rendered. */
|
|
171
|
+
macroId: string | undefined;
|
|
172
|
+
versions: SkillVersion[];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* In-flight state for authoring. Separate from `skillDetail` because a save keeps the
|
|
176
|
+
* skill on screen while it runs — the form needs to disable itself and show the error
|
|
177
|
+
* without the prompt it is editing disappearing underneath.
|
|
178
|
+
*/
|
|
179
|
+
export interface SkillMutationState extends FetchStateAndError {
|
|
180
|
+
/**
|
|
181
|
+
* The skill the last successful save produced, so the caller can select it without
|
|
182
|
+
* refetching the list. Undefined until a save succeeds.
|
|
183
|
+
*/
|
|
184
|
+
savedMacroId: string | undefined;
|
|
185
|
+
}
|
|
186
|
+
export interface SkillDetailState extends FetchStateAndError {
|
|
187
|
+
detail: SkillDetail | undefined;
|
|
188
|
+
/** Which skill `detail` belongs to, so a stale response is never rendered. */
|
|
189
|
+
macroId: string | undefined;
|
|
56
190
|
}
|
|
57
191
|
export interface AiCfoViewState extends FetchStateAndError {
|
|
58
192
|
allChatSessionIds: ID[];
|
|
@@ -67,8 +201,16 @@ export interface AiCfoViewState extends FetchStateAndError {
|
|
|
67
201
|
reasoning: FetchStateAndError;
|
|
68
202
|
thought: FetchStateAndError;
|
|
69
203
|
}>;
|
|
70
|
-
/**
|
|
204
|
+
/** The skill whose prompt is open in the Skills browser. */
|
|
205
|
+
skillDetail: SkillDetailState;
|
|
206
|
+
/** Create / save-new-version / delete, while one is in flight. */
|
|
207
|
+
skillMutation: SkillMutationState;
|
|
208
|
+
/** Chat-visible skills for the `/` menu and the Skills browser. */
|
|
71
209
|
skills: SkillsState;
|
|
210
|
+
/** A historical version's prompt, while a restore is being set up. */
|
|
211
|
+
skillVersionBody: SkillVersionBodyState;
|
|
212
|
+
/** Version history for the skill open in the browser. */
|
|
213
|
+
skillVersions: SkillVersionsState;
|
|
72
214
|
stopSubmitStateByChatSessionId: Record<ID, FetchStateAndError>;
|
|
73
215
|
/** Keyed by `page_context` query value (e.g. balance_sheet). */
|
|
74
216
|
suggestedQuestionsByPageContext: Partial<Record<AiCfoSuggestedQuestionsPageContext, SuggestedQuestionsForPageContext>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../../zeniAPI';
|
|
5
|
+
import { deleteSkill, deleteSkillFailure, deleteSkillSuccess } from '../aiCfoViewReducer';
|
|
6
|
+
export type ActionType = ReturnType<typeof deleteSkill> | ReturnType<typeof deleteSkillSuccess> | ReturnType<typeof deleteSkillFailure>;
|
|
7
|
+
/** DELETE /1.0/skills/{macroId} — soft-deletes every version, frees the name. */
|
|
8
|
+
export declare const deleteSkillEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteSkillEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
+
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
/** DELETE /1.0/skills/{macroId} — soft-deletes every version, frees the name. */
|
|
9
|
+
const deleteSkillEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.deleteSkill.match),
|
|
10
|
+
// mergeMap for the same reason as the save: cancelling a delete does not un-delete
|
|
11
|
+
// it, and two deletes are for different skills anyway.
|
|
12
|
+
(0, operators_1.mergeMap)((action) => {
|
|
13
|
+
const { macroId } = action.payload;
|
|
14
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}`;
|
|
15
|
+
return zeniAPI.deleteAndGetJSON(url).pipe((0, operators_1.mergeMap)((response) => {
|
|
16
|
+
// isSuccessStatus, NOT isSuccessResponse: the latter also requires a non-null
|
|
17
|
+
// `data`, and a successful DELETE here is a 204 with no body. Using it reported
|
|
18
|
+
// every completed delete as a failure — the row stayed on screen and a retry
|
|
19
|
+
// 404'd on a skill that was already gone. deleteChatSessionEpic uses the status
|
|
20
|
+
// check for the same reason.
|
|
21
|
+
if ((0, responsePayload_1.isSuccessStatus)(response)) {
|
|
22
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.deleteSkillSuccess)({ macroId }));
|
|
23
|
+
}
|
|
24
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.deleteSkillFailure)(response.status));
|
|
25
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, aiCfoViewReducer_1.deleteSkillFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Delete skill REST API call failed', error instanceof Error ? { message: error.message } : undefined)))));
|
|
26
|
+
}));
|
|
27
|
+
exports.deleteSkillEpic = deleteSkillEpic;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../../zeniAPI';
|
|
5
|
+
import { fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess } from '../aiCfoViewReducer';
|
|
6
|
+
export type ActionType = ReturnType<typeof fetchSkillDetail> | ReturnType<typeof fetchSkillDetailSuccess> | ReturnType<typeof fetchSkillDetailFailure>;
|
|
7
|
+
/** GET {aiCfoMicroServiceBaseUrl}/1.0/skills/{macroId} — one skill's prompt + card. */
|
|
8
|
+
export declare const fetchSkillDetailEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchSkillDetailEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
+
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
const toSkillDetail_1 = require("./toSkillDetail");
|
|
9
|
+
// Deliberately minimal: the browser already holds the full Skill from the list, and
|
|
10
|
+
// this response's `skill` block exists so a deep link can open a detail view without
|
|
11
|
+
// the list ever having been fetched.
|
|
12
|
+
/** GET {aiCfoMicroServiceBaseUrl}/1.0/skills/{macroId} — one skill's prompt + card. */
|
|
13
|
+
const fetchSkillDetailEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.fetchSkillDetail.match),
|
|
14
|
+
// switchMap: clicking through the list fires one of these per skill, and only the
|
|
15
|
+
// one the user is currently looking at matters. The reducer also drops responses
|
|
16
|
+
// whose macroId no longer matches, so a cancelled-but-in-flight request cannot
|
|
17
|
+
// render the wrong prompt under the right title.
|
|
18
|
+
(0, operators_1.switchMap)((action) => {
|
|
19
|
+
const { macroId } = action.payload;
|
|
20
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}`;
|
|
21
|
+
return zeniAPI.getJSON(url).pipe((0, operators_1.mergeMap)((response) => {
|
|
22
|
+
// Shared mapper and shared guard. This epic used to keep its own `toSkill`
|
|
23
|
+
// and default a missing prompt to '', so the same malformed body showed an
|
|
24
|
+
// empty prompt here and failed the read elsewhere — and `isMine` was nearly
|
|
25
|
+
// added to only two of the three copies.
|
|
26
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) &&
|
|
27
|
+
(0, toSkillDetail_1.isSkillDetailPayload)(response.data)) {
|
|
28
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillDetailSuccess)({
|
|
29
|
+
detail: (0, toSkillDetail_1.toSkillDetail)(response.data),
|
|
30
|
+
macroId,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillDetailFailure)({ error: response.status, macroId }));
|
|
34
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillDetailFailure)({
|
|
35
|
+
error: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch skill detail REST API call failed', error instanceof Error ? { message: error.message } : undefined),
|
|
36
|
+
macroId,
|
|
37
|
+
}))));
|
|
38
|
+
}));
|
|
39
|
+
exports.fetchSkillDetailEpic = fetchSkillDetailEpic;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../../zeniAPI';
|
|
5
|
+
import { fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess } from '../aiCfoViewReducer';
|
|
6
|
+
export type ActionType = ReturnType<typeof fetchSkillVersionBody> | ReturnType<typeof fetchSkillVersionBodySuccess> | ReturnType<typeof fetchSkillVersionBodyFailure>;
|
|
7
|
+
/**
|
|
8
|
+
* GET /1.0/skills/{macroId}?version=N — one historical version's prompt.
|
|
9
|
+
*
|
|
10
|
+
* Only used to seed a restore. The history list carries metadata only, deliberately: a
|
|
11
|
+
* 20-version history would otherwise be a 240kB response for a panel that shows dates.
|
|
12
|
+
*/
|
|
13
|
+
export declare const fetchSkillVersionBodyEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchSkillVersionBodyEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
+
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
const toSkillDetail_1 = require("./toSkillDetail");
|
|
9
|
+
/**
|
|
10
|
+
* GET /1.0/skills/{macroId}?version=N — one historical version's prompt.
|
|
11
|
+
*
|
|
12
|
+
* Only used to seed a restore. The history list carries metadata only, deliberately: a
|
|
13
|
+
* 20-version history would otherwise be a 240kB response for a panel that shows dates.
|
|
14
|
+
*/
|
|
15
|
+
const fetchSkillVersionBodyEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.fetchSkillVersionBody.match),
|
|
16
|
+
// switchMap: a read, and only the version the user last clicked matters. The reducer
|
|
17
|
+
// also drops a response whose (macroId, version) no longer matches, so a
|
|
18
|
+
// cancelled-but-in-flight request cannot seed the form with the wrong body.
|
|
19
|
+
(0, operators_1.switchMap)((action) => {
|
|
20
|
+
const { macroId, version } = action.payload;
|
|
21
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}?version=${encodeURIComponent(String(version))}`;
|
|
22
|
+
return zeniAPI.getJSON(url).pipe((0, operators_1.mergeMap)((response) => {
|
|
23
|
+
// `instructions` checked, not just `data`: the payload TYPE says the field is
|
|
24
|
+
// required, and the network is under no obligation to agree. Without this the
|
|
25
|
+
// epic reported success with `instructions: undefined`, and the browser — which
|
|
26
|
+
// holds the editor closed until a body arrives — showed "Loading this version…"
|
|
27
|
+
// for ever, with no error and no way forward.
|
|
28
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && (0, toSkillDetail_1.hasPromptBody)(response.data)) {
|
|
29
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillVersionBodySuccess)({
|
|
30
|
+
macroId,
|
|
31
|
+
version,
|
|
32
|
+
instructions: response.data.instructions,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
// A malformed 2xx is a failure. An empty STRING is not malformed and is allowed
|
|
36
|
+
// through: the editor's own minimum-length rule refuses to save it, so the user
|
|
37
|
+
// sees a validation message on a form they can fix rather than a dead end.
|
|
38
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillVersionBodyFailure)({
|
|
39
|
+
macroId,
|
|
40
|
+
version,
|
|
41
|
+
error: response.status,
|
|
42
|
+
}));
|
|
43
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillVersionBodyFailure)({
|
|
44
|
+
macroId,
|
|
45
|
+
version,
|
|
46
|
+
error: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch skill version body REST API call failed', error instanceof Error ? { message: error.message } : undefined),
|
|
47
|
+
}))));
|
|
48
|
+
}));
|
|
49
|
+
exports.fetchSkillVersionBodyEpic = fetchSkillVersionBodyEpic;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../../zeniAPI';
|
|
5
|
+
import { fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess } from '../aiCfoViewReducer';
|
|
6
|
+
export type ActionType = ReturnType<typeof fetchSkillVersions> | ReturnType<typeof fetchSkillVersionsSuccess> | ReturnType<typeof fetchSkillVersionsFailure>;
|
|
7
|
+
/** GET /1.0/skills/{macroId}/versions — the history panel. */
|
|
8
|
+
export declare const fetchSkillVersionsEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchSkillVersionsEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
+
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
const toVersion = (payload) => ({
|
|
9
|
+
version: payload.version,
|
|
10
|
+
semver: payload.semver ?? null,
|
|
11
|
+
displayName: payload.display_name ?? null,
|
|
12
|
+
updatedAt: payload.updated_at ?? null,
|
|
13
|
+
// Defaulted defensively: these are booleans the server always sends, but a malformed
|
|
14
|
+
// 2xx should render a row as inactive-and-not-latest rather than as the live one.
|
|
15
|
+
isActive: payload.is_active === true,
|
|
16
|
+
isLatest: payload.is_latest === true,
|
|
17
|
+
instructionsCharCount: payload.instructions_char_count ?? 0,
|
|
18
|
+
});
|
|
19
|
+
/** GET /1.0/skills/{macroId}/versions — the history panel. */
|
|
20
|
+
const fetchSkillVersionsEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.fetchSkillVersions.match),
|
|
21
|
+
// switchMap: a read, and only the skill currently open matters. The reducer also
|
|
22
|
+
// drops responses whose macroId no longer matches, so a cancelled-but-in-flight
|
|
23
|
+
// request cannot render one skill's history under another's title.
|
|
24
|
+
(0, operators_1.switchMap)((action) => {
|
|
25
|
+
const { macroId } = action.payload;
|
|
26
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}/versions`;
|
|
27
|
+
return zeniAPI.getJSON(url).pipe((0, operators_1.mergeMap)((response) => {
|
|
28
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
29
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillVersionsSuccess)({
|
|
30
|
+
macroId,
|
|
31
|
+
// Guard the array: a malformed 2xx without `versions` must not crash .map.
|
|
32
|
+
versions: (response.data.versions ?? []).map(toVersion),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillVersionsFailure)({ macroId, error: response.status }));
|
|
36
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillVersionsFailure)({
|
|
37
|
+
macroId,
|
|
38
|
+
error: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch skill versions REST API call failed', error instanceof Error ? { message: error.message } : undefined),
|
|
39
|
+
}))));
|
|
40
|
+
}));
|
|
41
|
+
exports.fetchSkillVersionsEpic = fetchSkillVersionsEpic;
|
|
@@ -5,6 +5,7 @@ const rxjs_1 = require("rxjs");
|
|
|
5
5
|
const operators_1 = require("rxjs/operators");
|
|
6
6
|
const responsePayload_1 = require("../../../responsePayload");
|
|
7
7
|
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
const toSkillDetail_1 = require("./toSkillDetail");
|
|
8
9
|
const isNonBlankString = (value) => typeof value === 'string' && value.trim() !== '';
|
|
9
10
|
// parameter_hints is a free-form {param: exampleValue(s)} map; flatten its non-blank
|
|
10
11
|
// string values (a single string or an array of them) into display-ready "try: …"
|
|
@@ -30,14 +31,12 @@ const toExampleHints = (parameterHints) => {
|
|
|
30
31
|
// a broken duplicate to the user — dedupe across parameter keys and array entries.
|
|
31
32
|
return Array.from(new Set(hints));
|
|
32
33
|
};
|
|
34
|
+
// Only the LIST route returns parameter_hints, so this is the one call site that has
|
|
35
|
+
// hints to spread in. Everything else about a summary comes from the shared base mapper,
|
|
36
|
+
// so a new `Skill` field cannot land here and be missed on the detail path.
|
|
33
37
|
const toSkill = (skillSummaryPayload) => ({
|
|
34
|
-
|
|
35
|
-
name: skillSummaryPayload.name,
|
|
36
|
-
displayName: skillSummaryPayload.display_name,
|
|
37
|
-
description: skillSummaryPayload.description,
|
|
38
|
-
semver: skillSummaryPayload.semver,
|
|
38
|
+
...(0, toSkillDetail_1.toBaseSkill)(skillSummaryPayload),
|
|
39
39
|
exampleHints: toExampleHints(skillSummaryPayload.parameter_hints),
|
|
40
|
-
hasScripts: skillSummaryPayload.has_scripts,
|
|
41
40
|
});
|
|
42
41
|
/** GET {aiCfoMicroServiceBaseUrl}/1.0/skills — chat-visible skills for the `/` menu. */
|
|
43
42
|
const fetchSkillsEpic = (actions$,
|
|
@@ -55,6 +54,12 @@ _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.fe
|
|
|
55
54
|
// Guard the array: a malformed 2xx body without `skills` must not crash .map.
|
|
56
55
|
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillsSuccess)({
|
|
57
56
|
skills: (response.data.skills ?? []).map(toSkill),
|
|
57
|
+
// === true, not a truthy check: a body missing the field entirely
|
|
58
|
+
// must read as "off" so the Skills entry stays hidden by default.
|
|
59
|
+
skillsEnabled: response.data.skills_enabled === true,
|
|
60
|
+
// Same discipline: a body without the field must withhold the publish
|
|
61
|
+
// control, not grant it.
|
|
62
|
+
canPublish: response.data.can_publish === true,
|
|
58
63
|
}));
|
|
59
64
|
}
|
|
60
65
|
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.fetchSkillsFailure)(response.status));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../../zeniAPI';
|
|
5
|
+
import { fetchSkillVersions, fetchSkills, saveSkill, saveSkillFailure, saveSkillSuccess } from '../aiCfoViewReducer';
|
|
6
|
+
export type ActionType = ReturnType<typeof saveSkill> | ReturnType<typeof saveSkillSuccess> | ReturnType<typeof saveSkillFailure> | ReturnType<typeof fetchSkills> | ReturnType<typeof fetchSkillVersions>;
|
|
7
|
+
/**
|
|
8
|
+
* POST /1.0/skills (create) or PUT /1.0/skills/{macroId} (new version).
|
|
9
|
+
*
|
|
10
|
+
* One epic for both because they are the same action to a user — "save what I wrote" —
|
|
11
|
+
* and the server answers both with the detail payload. Splitting them would duplicate
|
|
12
|
+
* the payload mapping and the error handling for a difference the form does not have.
|
|
13
|
+
*/
|
|
14
|
+
export declare const saveSkillEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.saveSkillEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
+
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
const aiCfoViewSelector_1 = require("../aiCfoViewSelector");
|
|
9
|
+
const toSkillDetail_1 = require("./toSkillDetail");
|
|
10
|
+
/**
|
|
11
|
+
* POST /1.0/skills (create) or PUT /1.0/skills/{macroId} (new version).
|
|
12
|
+
*
|
|
13
|
+
* One epic for both because they are the same action to a user — "save what I wrote" —
|
|
14
|
+
* and the server answers both with the detail payload. Splitting them would duplicate
|
|
15
|
+
* the payload mapping and the error handling for a difference the form does not have.
|
|
16
|
+
*/
|
|
17
|
+
const saveSkillEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.saveSkill.match),
|
|
18
|
+
// mergeMap, NOT switchMap: a save is a write. Cancelling an in-flight one because
|
|
19
|
+
// another started does not un-send it, it just hides the outcome — and for a
|
|
20
|
+
// create that means a skill exists that the user was never told about.
|
|
21
|
+
(0, operators_1.mergeMap)((action) => {
|
|
22
|
+
const { draft, macroId } = action.payload;
|
|
23
|
+
const base = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills`;
|
|
24
|
+
const body = {
|
|
25
|
+
name: draft.name,
|
|
26
|
+
instructions: draft.instructions,
|
|
27
|
+
description: draft.description,
|
|
28
|
+
// Sent as the menu name too. The server derives the unique `/` slug from it, so
|
|
29
|
+
// letting the two drift would make the command stop matching the row's title.
|
|
30
|
+
display_name: draft.name,
|
|
31
|
+
// Spread, not `visibility: draft.visibility`: the server distinguishes an ABSENT
|
|
32
|
+
// visibility ("leave it alone") from an explicit null or 'private' ("make it
|
|
33
|
+
// private"). Sending the key with a null value would unpublish a tenant-wide
|
|
34
|
+
// skill on any edit by someone who cannot publish.
|
|
35
|
+
//
|
|
36
|
+
// `!= null`, not `!== null`: the type says this is always present, but the value
|
|
37
|
+
// arrives from a component, and an untyped caller omitting it would spread
|
|
38
|
+
// `{visibility: undefined}` — a key present with no value, which is the one shape
|
|
39
|
+
// the rule above is about. The looser check costs nothing and closes it.
|
|
40
|
+
...(draft.visibility != null && { visibility: draft.visibility }),
|
|
41
|
+
};
|
|
42
|
+
const request$ = macroId != null && macroId !== ''
|
|
43
|
+
? zeniAPI.putAndGetJSON(`${base}/${encodeURIComponent(macroId)}`, body)
|
|
44
|
+
: zeniAPI.postAndGetJSON(base, body);
|
|
45
|
+
return request$.pipe((0, operators_1.mergeMap)((response) => {
|
|
46
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) &&
|
|
47
|
+
(0, toSkillDetail_1.isSkillDetailPayload)(response.data)) {
|
|
48
|
+
const saved = response.data;
|
|
49
|
+
const savedMacroId = saved.skill.macro_id;
|
|
50
|
+
const emitted = [
|
|
51
|
+
(0, aiCfoViewReducer_1.saveSkillSuccess)({
|
|
52
|
+
macroId: savedMacroId,
|
|
53
|
+
// The response is a full detail read — carried through so an editor left
|
|
54
|
+
// on this skill shows what it just saved rather than the previous prompt.
|
|
55
|
+
detail: (0, toSkillDetail_1.toSkillDetail)(saved),
|
|
56
|
+
}),
|
|
57
|
+
// Refetch rather than splice the response in: a create changes the list's
|
|
58
|
+
// ordering and an update can change the name it is sorted by, so a local
|
|
59
|
+
// insert would put the row somewhere the next load moves it away from.
|
|
60
|
+
(0, aiCfoViewReducer_1.fetchSkills)(),
|
|
61
|
+
];
|
|
62
|
+
// A save adds a version, so the history panel is one behind — but only
|
|
63
|
+
// refetch when this skill is still the one on screen. `fetchSkillVersions`
|
|
64
|
+
// takes ownership of the versions slice, so firing it for a skill the user
|
|
65
|
+
// has already left would cancel the history of whatever they opened next.
|
|
66
|
+
if ((0, aiCfoViewSelector_1.getSkillDetail)(state$.value).macroId === savedMacroId) {
|
|
67
|
+
emitted.push((0, aiCfoViewReducer_1.fetchSkillVersions)({ macroId: savedMacroId }));
|
|
68
|
+
}
|
|
69
|
+
return (0, rxjs_1.of)(...emitted);
|
|
70
|
+
}
|
|
71
|
+
// A malformed 2xx is a failure, not a silent success: reporting "saved" when
|
|
72
|
+
// no skill came back leaves the user believing work was persisted.
|
|
73
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.saveSkillFailure)(response.status));
|
|
74
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, aiCfoViewReducer_1.saveSkillFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Save skill REST API call failed', error instanceof Error ? { message: error.message } : undefined)))));
|
|
75
|
+
}));
|
|
76
|
+
exports.saveSkillEpic = saveSkillEpic;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../../zeniAPI';
|
|
5
|
+
import { fetchSkills, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess } from '../aiCfoViewReducer';
|
|
6
|
+
export type ActionType = ReturnType<typeof setSkillVisibility> | ReturnType<typeof setSkillVisibilitySuccess> | ReturnType<typeof setSkillVisibilityFailure> | ReturnType<typeof fetchSkills>;
|
|
7
|
+
/**
|
|
8
|
+
* PUT /1.0/skills/{macroId}/visibility — publish or unpublish, without a new version.
|
|
9
|
+
*
|
|
10
|
+
* Its own route rather than the prompt PUT: that one requires name and instructions, so a
|
|
11
|
+
* visibility-only change cannot be expressed through it, and sending the whole prompt back
|
|
12
|
+
* to flip one field would create a version whose prompt is identical to its parent's.
|
|
13
|
+
*/
|
|
14
|
+
export declare const setSkillVisibilityEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setSkillVisibilityEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
+
const aiCfoViewReducer_1 = require("../aiCfoViewReducer");
|
|
8
|
+
const toSkillDetail_1 = require("./toSkillDetail");
|
|
9
|
+
/**
|
|
10
|
+
* PUT /1.0/skills/{macroId}/visibility — publish or unpublish, without a new version.
|
|
11
|
+
*
|
|
12
|
+
* Its own route rather than the prompt PUT: that one requires name and instructions, so a
|
|
13
|
+
* visibility-only change cannot be expressed through it, and sending the whole prompt back
|
|
14
|
+
* to flip one field would create a version whose prompt is identical to its parent's.
|
|
15
|
+
*/
|
|
16
|
+
const setSkillVisibilityEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(aiCfoViewReducer_1.setSkillVisibility.match),
|
|
17
|
+
// mergeMap, like the other writes: cancelling a write in flight does not un-send it,
|
|
18
|
+
// it only hides the outcome — and for a publish that means a skill sitting in every
|
|
19
|
+
// colleague's menu with nobody told it got there.
|
|
20
|
+
(0, operators_1.mergeMap)((action) => {
|
|
21
|
+
const { macroId, visibility } = action.payload;
|
|
22
|
+
const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}/visibility`;
|
|
23
|
+
return zeniAPI.putAndGetJSON(url, { visibility }).pipe((0, operators_1.mergeMap)((response) => {
|
|
24
|
+
if ((0, responsePayload_1.isSuccessResponse)(response) &&
|
|
25
|
+
(0, toSkillDetail_1.isSkillDetailPayload)(response.data)) {
|
|
26
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.setSkillVisibilitySuccess)({
|
|
27
|
+
macroId,
|
|
28
|
+
detail: (0, toSkillDetail_1.toSkillDetail)(response.data),
|
|
29
|
+
}),
|
|
30
|
+
// The list carries the badge that shows the audience, so it is now stale.
|
|
31
|
+
(0, aiCfoViewReducer_1.fetchSkills)());
|
|
32
|
+
}
|
|
33
|
+
return (0, rxjs_1.of)((0, aiCfoViewReducer_1.setSkillVisibilityFailure)(response.status));
|
|
34
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, aiCfoViewReducer_1.setSkillVisibilityFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Set skill visibility REST API call failed', error instanceof Error ? { message: error.message } : undefined)))));
|
|
35
|
+
}));
|
|
36
|
+
exports.setSkillVisibilityEpic = setSkillVisibilityEpic;
|