@aglyn/plugins-ai 1.0.0-beta.234 → 1.0.0-beta.235
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/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +25 -0
- package/src/lib/activity/ai-activity-actions.js +85 -4
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.js +46 -2
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-campaign-create.component.js +1 -1
- package/src/lib/components/ai-campaign-create.component.js.map +1 -1
- package/src/lib/components/ai-credits-card.component.js +3 -3
- package/src/lib/components/ai-credits-card.component.js.map +1 -1
- package/src/lib/components/ai-crm-email-draft.component.js +1 -1
- package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
- package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
- package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
- package/src/lib/components/ai-crm-record-card.component.js +1 -1
- package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +4 -2
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-org-automation.component.js +1 -1
- package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +2 -2
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.js +1 -1
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/ai-insight-card.component.js +3 -1
- package/src/lib/components/ai-insight-card.component.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.d.ts +6 -1
- package/src/lib/components/ai-jobs-list-page.component.js +15 -2
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-logic.component.js +5 -3
- package/src/lib/components/ai-logic.component.js.map +1 -1
- package/src/lib/components/ai-media-create.component.js +88 -16
- package/src/lib/components/ai-media-create.component.js.map +1 -1
- package/src/lib/components/ai-model-selector.component.js +8 -0
- package/src/lib/components/ai-model-selector.component.js.map +1 -1
- package/src/lib/components/ai-overlay-cards.component.js +2 -2
- package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.js +2 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +2 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-revise-automation.component.js +1 -1
- package/src/lib/components/ai-revise-automation.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js +7 -3
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-site-batch-card.component.js +7 -4
- package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +7 -3
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
- package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +62 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +17 -3
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.js +13 -2
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.js +20 -3
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +2 -2
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-ai-top-users.component.js +1 -0
- package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +2 -2
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/components/host-ai-allotments.component.js +2 -2
- package/src/lib/components/host-ai-allotments.component.js.map +1 -1
- package/src/lib/components/member-ai-allotment-card.component.js +2 -2
- package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +1 -0
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
- package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
- package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +4 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +12 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +20 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +10 -1
- package/src/lib/jobs/ai-job-page-language.js +41 -8
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +14 -3
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +75 -7
- package/src/lib/jobs/ai-job-site-content.js +205 -32
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +8 -0
- package/src/lib/jobs/ai-job-site-step.js +61 -12
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-compiler.js +308 -10
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +7 -0
- package/src/lib/layout-language/ai-layout-design.js +23 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-frame.js +12 -3
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +18 -2
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +26 -1
- package/src/lib/layout-language/ai-layout-links.js +24 -4
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
- package/src/lib/layout-language/ai-layout-listings.js +187 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
- package/src/lib/model/ai-site-build-progress.js +27 -12
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +33 -8
- package/src/lib/model/ai-site-job.js +38 -18
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +30 -8
- package/src/lib/model/ai-site-kinds.js +731 -117
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +30 -0
- package/src/lib/model/ai-site-start-seo.js +132 -11
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +2 -2
- package/src/lib/model/ai-site-start.js +3 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/plugin.js +15 -3
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +13 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-admin-conversations.d.ts +9 -0
- package/src/lib/server/ai-admin-conversations.js +151 -9
- package/src/lib/server/ai-admin-conversations.js.map +1 -1
- package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
- package/src/lib/usage/staff-org-ai-conversations.js +45 -0
- package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
|
@@ -16,7 +16,16 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type StaffAiConversationKind, type StaffAiConversationsResponse } from '../usage/staff-org-ai-conversations';
|
|
18
18
|
type Firestore = FirebaseFirestore.Firestore;
|
|
19
|
+
/**
|
|
20
|
+
* One page of what ONE ACCOUNT asked Aglyn AI (AGL-3660), across every
|
|
21
|
+
* organization it has asked in — including one it has since left, which is
|
|
22
|
+
* the history a support question about it needs. Collection-group reads on
|
|
23
|
+
* the creator, newest first (`firebase-firestore.indexes.json`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function readStaffAccountAiRequests(db: Firestore, uid: string, kind: StaffAiConversationKind, after: string | null): Promise<StaffAiConversationsResponse>;
|
|
19
26
|
/** One page of the org's conversations of `kind`. Exported for the spec. */
|
|
20
27
|
export declare function readStaffAiConversations(db: Firestore, orgId: string, kind: StaffAiConversationKind, after: string | null): Promise<StaffAiConversationsResponse>;
|
|
28
|
+
/** One job of an org, as a one-row page — what an activity row's job link opens. */
|
|
29
|
+
export declare function readStaffAiJob(db: Firestore, orgId: string, jobId: string): Promise<StaffAiConversationsResponse>;
|
|
21
30
|
declare function handler(request: Request): Promise<Response>;
|
|
22
31
|
export { handler as GET };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -36,6 +37,7 @@ import { composeStaffAiAssistRow, composeStaffAiJobRow, isStaffAiConversationKin
|
|
|
36
37
|
* its brief or of what it wrote", so the row carries none of it.
|
|
37
38
|
*/ /** Members read to name who asked; past it a row shows the uid alone. */ const MEMBERS_SCAN = 500;
|
|
38
39
|
const ACTION = 'org.ai-conversations-viewed';
|
|
40
|
+
/** The same read, of one account's requests across every org (AGL-3660). */ const USER_ACTION = 'user.ai-requests-viewed';
|
|
39
41
|
/** One page of a collection under the org, newest first, after the document `after` names. */ async function readPage(orgRef, collection, after) {
|
|
40
42
|
var _ref;
|
|
41
43
|
var _docs_;
|
|
@@ -73,6 +75,83 @@ async function readMembers(orgRef) {
|
|
|
73
75
|
];
|
|
74
76
|
}));
|
|
75
77
|
}
|
|
78
|
+
/** The org a document under `orgs/{orgId}/…` is filed in. */ const orgOf = (doc)=>{
|
|
79
|
+
var _ref;
|
|
80
|
+
var _doc_ref_parent_parent;
|
|
81
|
+
return (_ref = (_doc_ref_parent_parent = doc.ref.parent.parent) == null ? void 0 : _doc_ref_parent_parent.id) != null ? _ref : null;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* One page of what ONE ACCOUNT asked Aglyn AI (AGL-3660), across every
|
|
85
|
+
* organization it has asked in — including one it has since left, which is
|
|
86
|
+
* the history a support question about it needs. Collection-group reads on
|
|
87
|
+
* the creator, newest first (`firebase-firestore.indexes.json`).
|
|
88
|
+
*/ export async function readStaffAccountAiRequests(db, uid, kind, after) {
|
|
89
|
+
var _ref;
|
|
90
|
+
var _docs_;
|
|
91
|
+
const group = kind === 'assist' ? 'assistExchanges' : 'aiJobs';
|
|
92
|
+
const field = kind === 'assist' ? 'uid' : 'createdBy';
|
|
93
|
+
let query = db.collectionGroup(group).where(field, '==', uid).orderBy('createdAt', 'desc').limit(STAFF_AI_CONVERSATIONS_PAGE + 1);
|
|
94
|
+
if (after) {
|
|
95
|
+
// The cursor is the document's PATH: an id is unique only within one org.
|
|
96
|
+
const cursor = await db.doc(after).get();
|
|
97
|
+
if (cursor.exists) query = query.startAfter(cursor);
|
|
98
|
+
}
|
|
99
|
+
const snapshot = await query.get();
|
|
100
|
+
const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE);
|
|
101
|
+
const next = snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? (_ref = (_docs_ = docs[docs.length - 1]) == null ? void 0 : _docs_.ref.path) != null ? _ref : null : null;
|
|
102
|
+
const none = new Map();
|
|
103
|
+
if (kind === 'assist') {
|
|
104
|
+
const signals = await readAll(db, docs.map((doc)=>doc.ref.parent.parent.collection('assistSignals').doc(doc.id)));
|
|
105
|
+
return {
|
|
106
|
+
kind,
|
|
107
|
+
rows: docs.map((doc)=>{
|
|
108
|
+
var _doc_data, _signals_get;
|
|
109
|
+
return _extends({}, composeStaffAiAssistRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, (_signals_get = signals.get(doc.id)) != null ? _signals_get : null, none), {
|
|
110
|
+
orgId: orgOf(doc)
|
|
111
|
+
});
|
|
112
|
+
}),
|
|
113
|
+
next
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const results = await readJobResults(db, docs.map((doc)=>({
|
|
117
|
+
key: doc.ref.path,
|
|
118
|
+
orgRef: doc.ref.parent.parent,
|
|
119
|
+
id: doc.id,
|
|
120
|
+
kind: doc.get('kind')
|
|
121
|
+
})));
|
|
122
|
+
return {
|
|
123
|
+
kind,
|
|
124
|
+
rows: docs.map((doc)=>{
|
|
125
|
+
var _doc_data, _results_get;
|
|
126
|
+
return _extends({}, composeStaffAiJobRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, (_results_get = results.get(doc.ref.path)) != null ? _results_get : null, none), {
|
|
127
|
+
orgId: orgOf(doc)
|
|
128
|
+
});
|
|
129
|
+
}),
|
|
130
|
+
next
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** The insight or CRM answer kept beside each job that keeps one, by the caller's key. */ async function readJobResults(db, jobs) {
|
|
134
|
+
const wanted = jobs.flatMap((job)=>{
|
|
135
|
+
const collection = staffAiJobResultCollection(job.kind);
|
|
136
|
+
return collection ? [
|
|
137
|
+
_extends({}, job, {
|
|
138
|
+
collection,
|
|
139
|
+
ref: job.orgRef.collection(collection).doc(job.id)
|
|
140
|
+
})
|
|
141
|
+
] : [];
|
|
142
|
+
});
|
|
143
|
+
const out = new Map();
|
|
144
|
+
if (!wanted.length) return out;
|
|
145
|
+
// `getAll` answers in the order it was asked.
|
|
146
|
+
const snapshots = await db.getAll(...wanted.map((job)=>job.ref));
|
|
147
|
+
wanted.forEach((job, index)=>{
|
|
148
|
+
var _snapshot_data;
|
|
149
|
+
const snapshot = snapshots[index];
|
|
150
|
+
const data = (snapshot == null ? void 0 : snapshot.exists) ? (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {} : null;
|
|
151
|
+
out.set(job.key, job.collection === 'aiInsights' ? staffAiInsightResult(data) : staffAiCrmResult(data));
|
|
152
|
+
});
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
76
155
|
/** One page of the org's conversations of `kind`. Exported for the spec. */ export async function readStaffAiConversations(db, orgId, kind, after) {
|
|
77
156
|
const orgRef = db.collection('orgs').doc(orgId);
|
|
78
157
|
if (kind === 'assist') {
|
|
@@ -86,7 +165,9 @@ async function readMembers(orgRef) {
|
|
|
86
165
|
kind,
|
|
87
166
|
rows: docs.map((doc)=>{
|
|
88
167
|
var _doc_data, _signals_get;
|
|
89
|
-
return composeStaffAiAssistRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, (_signals_get = signals.get(doc.id)) != null ? _signals_get : null, members)
|
|
168
|
+
return _extends({}, composeStaffAiAssistRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, (_signals_get = signals.get(doc.id)) != null ? _signals_get : null, members), {
|
|
169
|
+
orgId
|
|
170
|
+
});
|
|
90
171
|
}),
|
|
91
172
|
next
|
|
92
173
|
};
|
|
@@ -107,13 +188,45 @@ async function readMembers(orgRef) {
|
|
|
107
188
|
var _insights_get, _crm_get, _doc_data;
|
|
108
189
|
const collection = staffAiJobResultCollection(doc.get('kind'));
|
|
109
190
|
const result = collection === 'aiInsights' ? staffAiInsightResult((_insights_get = insights.get(doc.id)) != null ? _insights_get : null) : collection === 'aiCrmAnswers' ? staffAiCrmResult((_crm_get = crm.get(doc.id)) != null ? _crm_get : null) : null;
|
|
110
|
-
return composeStaffAiJobRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, result, members)
|
|
191
|
+
return _extends({}, composeStaffAiJobRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, result, members), {
|
|
192
|
+
orgId
|
|
193
|
+
});
|
|
111
194
|
}),
|
|
112
195
|
next
|
|
113
196
|
};
|
|
114
197
|
}
|
|
198
|
+
/** One job of an org, as a one-row page — what an activity row's job link opens. */ export async function readStaffAiJob(db, orgId, jobId) {
|
|
199
|
+
var _doc_data, _results_get;
|
|
200
|
+
const orgRef = db.collection('orgs').doc(orgId);
|
|
201
|
+
const [doc, members] = await Promise.all([
|
|
202
|
+
orgRef.collection('aiJobs').doc(jobId).get(),
|
|
203
|
+
readMembers(orgRef)
|
|
204
|
+
]);
|
|
205
|
+
if (!doc.exists) return {
|
|
206
|
+
kind: 'jobs',
|
|
207
|
+
rows: [],
|
|
208
|
+
next: null
|
|
209
|
+
};
|
|
210
|
+
const results = await readJobResults(db, [
|
|
211
|
+
{
|
|
212
|
+
key: jobId,
|
|
213
|
+
orgRef,
|
|
214
|
+
id: jobId,
|
|
215
|
+
kind: doc.get('kind')
|
|
216
|
+
}
|
|
217
|
+
]);
|
|
218
|
+
return {
|
|
219
|
+
kind: 'jobs',
|
|
220
|
+
rows: [
|
|
221
|
+
_extends({}, composeStaffAiJobRow(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {}, (_results_get = results.get(jobId)) != null ? _results_get : null, members), {
|
|
222
|
+
orgId
|
|
223
|
+
})
|
|
224
|
+
],
|
|
225
|
+
next: null
|
|
226
|
+
};
|
|
227
|
+
}
|
|
115
228
|
async function handler(request) {
|
|
116
|
-
var _headers_authorization, _query_orgId;
|
|
229
|
+
var _headers_authorization, _query_orgId, _query_uid;
|
|
117
230
|
const { method, query, headers: rawHeaders } = await pluginRequestFromWeb(request);
|
|
118
231
|
const headers = rawHeaders;
|
|
119
232
|
if (method !== 'GET') {
|
|
@@ -131,11 +244,16 @@ async function handler(request) {
|
|
|
131
244
|
status: 401
|
|
132
245
|
});
|
|
133
246
|
const orgId = String((_query_orgId = query.orgId) != null ? _query_orgId : '');
|
|
134
|
-
|
|
135
|
-
|
|
247
|
+
// An account's requests across every org (AGL-3660), for the staff
|
|
248
|
+
// account page; exactly one of the two is the subject.
|
|
249
|
+
const uid = String((_query_uid = query.uid) != null ? _query_uid : '');
|
|
250
|
+
if (!orgId === !uid) return Response.json({
|
|
251
|
+
error: 'Pass orgId or uid'
|
|
136
252
|
}, {
|
|
137
253
|
status: 400
|
|
138
254
|
});
|
|
255
|
+
// One job, for a link from an activity or audit row to the job behind it.
|
|
256
|
+
const jobId = typeof query.jobId === 'string' && query.jobId ? query.jobId : null;
|
|
139
257
|
const kind = query.kind;
|
|
140
258
|
if (!isStaffAiConversationKind(kind)) {
|
|
141
259
|
return Response.json({
|
|
@@ -158,6 +276,28 @@ async function handler(request) {
|
|
|
158
276
|
});
|
|
159
277
|
}
|
|
160
278
|
const db = firebaseAdmin.app().firestore();
|
|
279
|
+
if (uid) {
|
|
280
|
+
const page = await readStaffAccountAiRequests(db, uid, kind, after);
|
|
281
|
+
// Written before the words leave, as for an org; filed against the
|
|
282
|
+
// account so its page's "Data access by staff" lists the read.
|
|
283
|
+
await addAdminAudit(db, {
|
|
284
|
+
actorUid: decoded.uid,
|
|
285
|
+
action: USER_ACTION,
|
|
286
|
+
target: `users/${uid}`,
|
|
287
|
+
subjectUid: uid,
|
|
288
|
+
before: null,
|
|
289
|
+
after: {
|
|
290
|
+
kind,
|
|
291
|
+
rows: page.rows.length,
|
|
292
|
+
after
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return Response.json(page, {
|
|
296
|
+
headers: {
|
|
297
|
+
'Cache-Control': 'no-store'
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
161
301
|
if (!(await db.collection('orgs').doc(orgId).get()).exists) {
|
|
162
302
|
return Response.json({
|
|
163
303
|
error: 'No such organization'
|
|
@@ -165,18 +305,20 @@ async function handler(request) {
|
|
|
165
305
|
status: 404
|
|
166
306
|
});
|
|
167
307
|
}
|
|
168
|
-
const page = await readStaffAiConversations(db, orgId, kind, after);
|
|
308
|
+
const page = jobId ? await readStaffAiJob(db, orgId, jobId) : await readStaffAiConversations(db, orgId, kind, after);
|
|
169
309
|
// Written before the words leave: a read the log cannot record is not served.
|
|
170
310
|
await addAdminAudit(db, {
|
|
171
311
|
actorUid: decoded.uid,
|
|
172
312
|
action: ACTION,
|
|
173
313
|
target: `orgs/${orgId}`,
|
|
174
314
|
before: null,
|
|
175
|
-
after: {
|
|
176
|
-
kind,
|
|
315
|
+
after: _extends({
|
|
316
|
+
kind: jobId ? 'jobs' : kind,
|
|
177
317
|
rows: page.rows.length,
|
|
178
318
|
after
|
|
179
|
-
}
|
|
319
|
+
}, jobId ? {
|
|
320
|
+
jobId
|
|
321
|
+
} : {})
|
|
180
322
|
});
|
|
181
323
|
return Response.json(page, {
|
|
182
324
|
headers: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-conversations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport {\n composeStaffAiAssistRow,\n composeStaffAiJobRow,\n isStaffAiConversationKind,\n STAFF_AI_CONVERSATIONS_PAGE,\n staffAiCrmResult,\n staffAiInsightResult,\n staffAiJobResultCollection,\n type StaffAiConversationKind,\n type StaffAiConversationsResponse,\n} from '../usage/staff-org-ai-conversations'\n\n/**\n * `/api/ai/admin/conversations` (AGL-3675): one page of what an org asked\n * Aglyn AI and what it answered, for the staff org page.\n *\n * `?orgId=` and `?kind=assist|jobs`, newest first, and `?after=<id>` for the\n * next page. Staff-gated exactly as `/api/ai/admin/org` is. The collections\n * it reads have no client rules at all — default-deny for every browser —\n * which is why the read is here, on the Admin SDK.\n *\n * ## Every page read is audited, and the audit holds no text\n *\n * Unlike the usage card, which loads with the page and so writes no audit\n * row, this route returns what a person typed, and staff read it only when\n * they ask to. Each page writes an `org.ai-conversations-viewed` access row\n * naming who read which list of which org and how many rows came back. The\n * Privacy policy says our audit log records an AI job \"without the text of\n * its brief or of what it wrote\", so the row carries none of it.\n */\n\n/** Members read to name who asked; past it a row shows the uid alone. */\nconst MEMBERS_SCAN = 500\n\nconst ACTION = 'org.ai-conversations-viewed'\n\ntype Firestore = FirebaseFirestore.Firestore\ntype OrgRef = FirebaseFirestore.DocumentReference\n\n/** One page of a collection under the org, newest first, after the document `after` names. */\nasync function readPage(\n orgRef: OrgRef,\n collection: 'assistExchanges' | 'aiJobs',\n after: string | null,\n): Promise<{ docs: FirebaseFirestore.QueryDocumentSnapshot[]; next: string | null }> {\n let query = orgRef.collection(collection).orderBy('createdAt', 'desc').limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n const cursor = await orgRef.collection(collection).doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next = snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? docs[docs.length - 1]?.id ?? null : null\n return { docs, next }\n}\n\nasync function readMembers(orgRef: OrgRef): Promise<Map<string, Record<string, unknown>>> {\n const snapshot = await orgRef.collection('members').limit(MEMBERS_SCAN).get()\n return new Map(snapshot.docs.map((doc) => [doc.id, doc.data() ?? {}]))\n}\n\n/** The documents `refs` name, by id, in one round trip; a missing one is absent. */\nasync function readAll(\n db: Firestore,\n refs: FirebaseFirestore.DocumentReference[],\n): Promise<Map<string, Record<string, unknown>>> {\n if (!refs.length) return new Map()\n const snapshots = await db.getAll(...refs)\n return new Map(\n snapshots.filter((snapshot) => snapshot.exists).map((snapshot) => [snapshot.id, snapshot.data() ?? {}]),\n )\n}\n\n/** One page of the org's conversations of `kind`. Exported for the spec. */\nexport async function readStaffAiConversations(\n db: Firestore,\n orgId: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n if (kind === 'assist') {\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'assistExchanges', after), readMembers(orgRef)])\n // The signal shares the exchange's id (AGL-1972): the route, the model, the cost and the thumbs.\n const signals = await readAll(db, docs.map((doc) => orgRef.collection('assistSignals').doc(doc.id)))\n return {\n kind,\n rows: docs.map((doc) => composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, members)),\n next,\n }\n }\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'aiJobs', after), readMembers(orgRef)])\n const insightIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiInsights').map((doc) => doc.id)\n const crmIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiCrmAnswers').map((doc) => doc.id)\n const [insights, crm] = await Promise.all([\n readAll(db, insightIds.map((id) => orgRef.collection('aiInsights').doc(id))),\n readAll(db, crmIds.map((id) => orgRef.collection('aiCrmAnswers').doc(id))),\n ])\n return {\n kind,\n rows: docs.map((doc) => {\n const collection = staffAiJobResultCollection(doc.get('kind'))\n const result =\n collection === 'aiInsights'\n ? staffAiInsightResult(insights.get(doc.id) ?? null)\n : collection === 'aiCrmAnswers'\n ? staffAiCrmResult(crm.get(doc.id) ?? null)\n : null\n return composeStaffAiJobRow(doc.id, doc.data() ?? {}, result, members)\n }),\n next,\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, query, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n if (!orgId) return Response.json({ error: 'Missing orgId' }, { status: 400 })\n const kind = query.kind\n if (!isStaffAiConversationKind(kind)) {\n return Response.json({ error: 'kind must be assist or jobs' }, { status: 400 })\n }\n const after = typeof query.after === 'string' && query.after ? query.after : null\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n const db = firebaseAdmin.app().firestore()\n if (!(await db.collection('orgs').doc(orgId).get()).exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n const page = await readStaffAiConversations(db, orgId, kind, after)\n // Written before the words leave: a read the log cannot record is not served.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: ACTION,\n target: `orgs/${orgId}`,\n before: null,\n after: { kind, rows: page.rows.length, after },\n })\n return Response.json(page, { headers: { 'Cache-Control': 'no-store' } })\n } catch (error) {\n console.error('[ai/admin/conversations]', error)\n return Response.json({ error: 'The conversations could not be read.' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","addAdminAudit","composeStaffAiAssistRow","composeStaffAiJobRow","isStaffAiConversationKind","STAFF_AI_CONVERSATIONS_PAGE","staffAiCrmResult","staffAiInsightResult","staffAiJobResultCollection","MEMBERS_SCAN","ACTION","readPage","orgRef","collection","after","docs","query","orderBy","limit","cursor","doc","get","exists","startAfter","snapshot","slice","next","length","id","readMembers","Map","map","data","readAll","db","refs","snapshots","getAll","filter","readStaffAiConversations","orgId","kind","members","Promise","all","signals","rows","insightIds","crmIds","insights","crm","result","handler","request","headers","method","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","undefined","String","decoded","app","auth","verifyIdToken","email_verified","firestore","page","actorUid","uid","action","target","before","console","GET"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-admin-conversations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport {\n composeStaffAiAssistRow,\n composeStaffAiJobRow,\n isStaffAiConversationKind,\n STAFF_AI_CONVERSATIONS_PAGE,\n staffAiCrmResult,\n staffAiInsightResult,\n staffAiJobResultCollection,\n type StaffAiJobResult,\n type StaffAiConversationKind,\n type StaffAiConversationsResponse,\n} from '../usage/staff-org-ai-conversations'\n\n/**\n * `/api/ai/admin/conversations` (AGL-3675): one page of what an org asked\n * Aglyn AI and what it answered, for the staff org page.\n *\n * `?orgId=` and `?kind=assist|jobs`, newest first, and `?after=<id>` for the\n * next page. Staff-gated exactly as `/api/ai/admin/org` is. The collections\n * it reads have no client rules at all — default-deny for every browser —\n * which is why the read is here, on the Admin SDK.\n *\n * ## Every page read is audited, and the audit holds no text\n *\n * Unlike the usage card, which loads with the page and so writes no audit\n * row, this route returns what a person typed, and staff read it only when\n * they ask to. Each page writes an `org.ai-conversations-viewed` access row\n * naming who read which list of which org and how many rows came back. The\n * Privacy policy says our audit log records an AI job \"without the text of\n * its brief or of what it wrote\", so the row carries none of it.\n */\n\n/** Members read to name who asked; past it a row shows the uid alone. */\nconst MEMBERS_SCAN = 500\n\nconst ACTION = 'org.ai-conversations-viewed'\n/** The same read, of one account's requests across every org (AGL-3660). */\nconst USER_ACTION = 'user.ai-requests-viewed'\n\ntype Firestore = FirebaseFirestore.Firestore\ntype OrgRef = FirebaseFirestore.DocumentReference\n\n/** One page of a collection under the org, newest first, after the document `after` names. */\nasync function readPage(\n orgRef: OrgRef,\n collection: 'assistExchanges' | 'aiJobs',\n after: string | null,\n): Promise<{ docs: FirebaseFirestore.QueryDocumentSnapshot[]; next: string | null }> {\n let query = orgRef.collection(collection).orderBy('createdAt', 'desc').limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n const cursor = await orgRef.collection(collection).doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next = snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? docs[docs.length - 1]?.id ?? null : null\n return { docs, next }\n}\n\nasync function readMembers(orgRef: OrgRef): Promise<Map<string, Record<string, unknown>>> {\n const snapshot = await orgRef.collection('members').limit(MEMBERS_SCAN).get()\n return new Map(snapshot.docs.map((doc) => [doc.id, doc.data() ?? {}]))\n}\n\n/** The documents `refs` name, by id, in one round trip; a missing one is absent. */\nasync function readAll(\n db: Firestore,\n refs: FirebaseFirestore.DocumentReference[],\n): Promise<Map<string, Record<string, unknown>>> {\n if (!refs.length) return new Map()\n const snapshots = await db.getAll(...refs)\n return new Map(\n snapshots.filter((snapshot) => snapshot.exists).map((snapshot) => [snapshot.id, snapshot.data() ?? {}]),\n )\n}\n\n/** The org a document under `orgs/{orgId}/…` is filed in. */\nconst orgOf = (doc: FirebaseFirestore.DocumentSnapshot): string | null =>\n doc.ref.parent.parent?.id ?? null\n\n/**\n * One page of what ONE ACCOUNT asked Aglyn AI (AGL-3660), across every\n * organization it has asked in — including one it has since left, which is\n * the history a support question about it needs. Collection-group reads on\n * the creator, newest first (`firebase-firestore.indexes.json`).\n */\nexport async function readStaffAccountAiRequests(\n db: Firestore,\n uid: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const group = kind === 'assist' ? 'assistExchanges' : 'aiJobs'\n const field = kind === 'assist' ? 'uid' : 'createdBy'\n let query = db\n .collectionGroup(group)\n .where(field, '==', uid)\n .orderBy('createdAt', 'desc')\n .limit(STAFF_AI_CONVERSATIONS_PAGE + 1)\n if (after) {\n // The cursor is the document's PATH: an id is unique only within one org.\n const cursor = await db.doc(after).get()\n if (cursor.exists) query = query.startAfter(cursor)\n }\n const snapshot = await query.get()\n const docs = snapshot.docs.slice(0, STAFF_AI_CONVERSATIONS_PAGE)\n const next =\n snapshot.docs.length > STAFF_AI_CONVERSATIONS_PAGE ? (docs[docs.length - 1]?.ref.path ?? null) : null\n const none = new Map<string, Record<string, unknown>>()\n if (kind === 'assist') {\n const signals = await readAll(\n db,\n docs.map((doc) => doc.ref.parent.parent!.collection('assistSignals').doc(doc.id)),\n )\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, none),\n orgId: orgOf(doc),\n })),\n next,\n }\n }\n const results = await readJobResults(\n db,\n docs.map((doc) => ({ key: doc.ref.path, orgRef: doc.ref.parent.parent!, id: doc.id, kind: doc.get('kind') })),\n )\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, results.get(doc.ref.path) ?? null, none),\n orgId: orgOf(doc),\n })),\n next,\n }\n}\n\n/** The insight or CRM answer kept beside each job that keeps one, by the caller's key. */\nasync function readJobResults(\n db: Firestore,\n jobs: ReadonlyArray<{ key: string; orgRef: OrgRef; id: string; kind: unknown }>,\n): Promise<Map<string, StaffAiJobResult | null>> {\n const wanted = jobs.flatMap((job) => {\n const collection = staffAiJobResultCollection(job.kind)\n return collection ? [{ ...job, collection, ref: job.orgRef.collection(collection).doc(job.id) }] : []\n })\n const out = new Map<string, StaffAiJobResult | null>()\n if (!wanted.length) return out\n // `getAll` answers in the order it was asked.\n const snapshots = await db.getAll(...wanted.map((job) => job.ref))\n wanted.forEach((job, index) => {\n const snapshot = snapshots[index]\n const data = snapshot?.exists ? (snapshot.data() ?? {}) : null\n out.set(job.key, job.collection === 'aiInsights' ? staffAiInsightResult(data) : staffAiCrmResult(data))\n })\n return out\n}\n\n/** One page of the org's conversations of `kind`. Exported for the spec. */\nexport async function readStaffAiConversations(\n db: Firestore,\n orgId: string,\n kind: StaffAiConversationKind,\n after: string | null,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n if (kind === 'assist') {\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'assistExchanges', after), readMembers(orgRef)])\n // The signal shares the exchange's id (AGL-1972): the route, the model, the cost and the thumbs.\n const signals = await readAll(db, docs.map((doc) => orgRef.collection('assistSignals').doc(doc.id)))\n return {\n kind,\n rows: docs.map((doc) => ({\n ...composeStaffAiAssistRow(doc.id, doc.data() ?? {}, signals.get(doc.id) ?? null, members),\n orgId,\n })),\n next,\n }\n }\n const [{ docs, next }, members] = await Promise.all([readPage(orgRef, 'aiJobs', after), readMembers(orgRef)])\n const insightIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiInsights').map((doc) => doc.id)\n const crmIds = docs.filter((doc) => staffAiJobResultCollection(doc.get('kind')) === 'aiCrmAnswers').map((doc) => doc.id)\n const [insights, crm] = await Promise.all([\n readAll(db, insightIds.map((id) => orgRef.collection('aiInsights').doc(id))),\n readAll(db, crmIds.map((id) => orgRef.collection('aiCrmAnswers').doc(id))),\n ])\n return {\n kind,\n rows: docs.map((doc) => {\n const collection = staffAiJobResultCollection(doc.get('kind'))\n const result =\n collection === 'aiInsights'\n ? staffAiInsightResult(insights.get(doc.id) ?? null)\n : collection === 'aiCrmAnswers'\n ? staffAiCrmResult(crm.get(doc.id) ?? null)\n : null\n return { ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, result, members), orgId }\n }),\n next,\n }\n}\n\n/** One job of an org, as a one-row page — what an activity row's job link opens. */\nexport async function readStaffAiJob(\n db: Firestore,\n orgId: string,\n jobId: string,\n): Promise<StaffAiConversationsResponse> {\n const orgRef = db.collection('orgs').doc(orgId)\n const [doc, members] = await Promise.all([orgRef.collection('aiJobs').doc(jobId).get(), readMembers(orgRef)])\n if (!doc.exists) return { kind: 'jobs', rows: [], next: null }\n const results = await readJobResults(db, [{ key: jobId, orgRef, id: jobId, kind: doc.get('kind') }])\n return {\n kind: 'jobs',\n rows: [{ ...composeStaffAiJobRow(doc.id, doc.data() ?? {}, results.get(jobId) ?? null, members), orgId }],\n next: null,\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, query, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n const orgId = String(query.orgId ?? '')\n // An account's requests across every org (AGL-3660), for the staff\n // account page; exactly one of the two is the subject.\n const uid = String(query.uid ?? '')\n if (!orgId === !uid) return Response.json({ error: 'Pass orgId or uid' }, { status: 400 })\n // One job, for a link from an activity or audit row to the job behind it.\n const jobId = typeof query.jobId === 'string' && query.jobId ? query.jobId : null\n const kind = query.kind\n if (!isStaffAiConversationKind(kind)) {\n return Response.json({ error: 'kind must be assist or jobs' }, { status: 400 })\n }\n const after = typeof query.after === 'string' && query.after ? query.after : null\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n if (!decoded['staff']) {\n return Response.json({ error: 'Staff only' }, { status: 403 })\n }\n const db = firebaseAdmin.app().firestore()\n if (uid) {\n const page = await readStaffAccountAiRequests(db, uid, kind, after)\n // Written before the words leave, as for an org; filed against the\n // account so its page's \"Data access by staff\" lists the read.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: USER_ACTION,\n target: `users/${uid}`,\n subjectUid: uid,\n before: null,\n after: { kind, rows: page.rows.length, after },\n })\n return Response.json(page, { headers: { 'Cache-Control': 'no-store' } })\n }\n if (!(await db.collection('orgs').doc(orgId).get()).exists) {\n return Response.json({ error: 'No such organization' }, { status: 404 })\n }\n const page = jobId\n ? await readStaffAiJob(db, orgId, jobId)\n : await readStaffAiConversations(db, orgId, kind, after)\n // Written before the words leave: a read the log cannot record is not served.\n await addAdminAudit(db, {\n actorUid: decoded.uid,\n action: ACTION,\n target: `orgs/${orgId}`,\n before: null,\n after: { kind: jobId ? 'jobs' : kind, rows: page.rows.length, after, ...(jobId ? { jobId } : {}) },\n })\n return Response.json(page, { headers: { 'Cache-Control': 'no-store' } })\n } catch (error) {\n console.error('[ai/admin/conversations]', error)\n return Response.json({ error: 'The conversations could not be read.' }, { status: 500 })\n }\n}\n\nexport { handler as GET }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","addAdminAudit","composeStaffAiAssistRow","composeStaffAiJobRow","isStaffAiConversationKind","STAFF_AI_CONVERSATIONS_PAGE","staffAiCrmResult","staffAiInsightResult","staffAiJobResultCollection","MEMBERS_SCAN","ACTION","USER_ACTION","readPage","orgRef","collection","after","docs","query","orderBy","limit","cursor","doc","get","exists","startAfter","snapshot","slice","next","length","id","readMembers","Map","map","data","readAll","db","refs","snapshots","getAll","filter","orgOf","ref","parent","readStaffAccountAiRequests","uid","kind","group","field","collectionGroup","where","path","none","signals","rows","orgId","results","readJobResults","key","jobs","wanted","flatMap","job","out","forEach","index","set","readStaffAiConversations","members","Promise","all","insightIds","crmIds","insights","crm","result","readStaffAiJob","jobId","handler","request","headers","method","rawHeaders","Response","json","error","status","authorization","idToken","startsWith","undefined","String","decoded","app","auth","verifyIdToken","email_verified","firestore","page","actorUid","action","target","subjectUid","before","console","GET"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,sBAAqB;AAC1D,SACEC,uBAAuB,EACvBC,aAAa,EACbC,sBAAsB,QACjB,2BAA0B;AACjC,SAASC,aAAa,QAAQ,oDAAmD;AACjF,SACEC,uBAAuB,EACvBC,oBAAoB,EACpBC,yBAAyB,EACzBC,2BAA2B,EAC3BC,gBAAgB,EAChBC,oBAAoB,EACpBC,0BAA0B,QAIrB,yCAAqC;AAE5C;;;;;;;;;;;;;;;;;CAiBC,GAED,uEAAuE,GACvE,MAAMC,eAAe;AAErB,MAAMC,SAAS;AACf,0EAA0E,GAC1E,MAAMC,cAAc;AAKpB,4FAA4F,GAC5F,eAAeC,SACbC,MAAc,EACdC,UAAwC,EACxCC,KAAoB;;QAS8CC;IAPlE,IAAIC,QAAQJ,OAAOC,UAAU,CAACA,YAAYI,OAAO,CAAC,aAAa,QAAQC,KAAK,CAACd,8BAA8B;IAC3G,IAAIU,OAAO;QACT,MAAMK,SAAS,MAAMP,OAAOC,UAAU,CAACA,YAAYO,GAAG,CAACN,OAAOO,GAAG;QACjE,IAAIF,OAAOG,MAAM,EAAEN,QAAQA,MAAMO,UAAU,CAACJ;IAC9C;IACA,MAAMK,WAAW,MAAMR,MAAMK,GAAG;IAChC,MAAMN,OAAOS,SAAST,IAAI,CAACU,KAAK,CAAC,GAAGrB;IACpC,MAAMsB,OAAOF,SAAST,IAAI,CAACY,MAAM,GAAGvB,uCAA8BW,SAAAA,IAAI,CAACA,KAAKY,MAAM,GAAG,EAAE,qBAArBZ,OAAuBa,EAAE,mBAAI,OAAO;IACtG,OAAO;QAAEb;QAAMW;IAAK;AACtB;AAEA,eAAeG,YAAYjB,MAAc;IACvC,MAAMY,WAAW,MAAMZ,OAAOC,UAAU,CAAC,WAAWK,KAAK,CAACV,cAAca,GAAG;IAC3E,OAAO,IAAIS,IAAIN,SAAST,IAAI,CAACgB,GAAG,CAAC,CAACX;YAAiBA;eAAT;YAACA,IAAIQ,EAAE;aAAER,YAAAA,IAAIY,IAAI,cAARZ,YAAc,CAAC;SAAE;;AACtE;AAEA,kFAAkF,GAClF,eAAea,QACbC,EAAa,EACbC,IAA2C;IAE3C,IAAI,CAACA,KAAKR,MAAM,EAAE,OAAO,IAAIG;IAC7B,MAAMM,YAAY,MAAMF,GAAGG,MAAM,IAAIF;IACrC,OAAO,IAAIL,IACTM,UAAUE,MAAM,CAAC,CAACd,WAAaA,SAASF,MAAM,EAAES,GAAG,CAAC,CAACP;YAA2BA;eAAd;YAACA,SAASI,EAAE;aAAEJ,iBAAAA,SAASQ,IAAI,cAAbR,iBAAmB,CAAC;SAAE;;AAE1G;AAEA,2DAA2D,GAC3D,MAAMe,QAAQ,CAACnB;;QACbA;oBAAAA,yBAAAA,IAAIoB,GAAG,CAACC,MAAM,CAACA,MAAM,qBAArBrB,uBAAuBQ,EAAE,mBAAI;;AAE/B;;;;;CAKC,GACD,OAAO,eAAec,2BACpBR,EAAa,EACbS,GAAW,EACXC,IAA6B,EAC7B9B,KAAoB;;QAiBoCC;IAfxD,MAAM8B,QAAQD,SAAS,WAAW,oBAAoB;IACtD,MAAME,QAAQF,SAAS,WAAW,QAAQ;IAC1C,IAAI5B,QAAQkB,GACTa,eAAe,CAACF,OAChBG,KAAK,CAACF,OAAO,MAAMH,KACnB1B,OAAO,CAAC,aAAa,QACrBC,KAAK,CAACd,8BAA8B;IACvC,IAAIU,OAAO;QACT,0EAA0E;QAC1E,MAAMK,SAAS,MAAMe,GAAGd,GAAG,CAACN,OAAOO,GAAG;QACtC,IAAIF,OAAOG,MAAM,EAAEN,QAAQA,MAAMO,UAAU,CAACJ;IAC9C;IACA,MAAMK,WAAW,MAAMR,MAAMK,GAAG;IAChC,MAAMN,OAAOS,SAAST,IAAI,CAACU,KAAK,CAAC,GAAGrB;IACpC,MAAMsB,OACJF,SAAST,IAAI,CAACY,MAAM,GAAGvB,uCAA+BW,SAAAA,IAAI,CAACA,KAAKY,MAAM,GAAG,EAAE,qBAArBZ,OAAuByB,GAAG,CAACS,IAAI,mBAAI,OAAQ;IACnG,MAAMC,OAAO,IAAIpB;IACjB,IAAIc,SAAS,UAAU;QACrB,MAAMO,UAAU,MAAMlB,QACpBC,IACAnB,KAAKgB,GAAG,CAAC,CAACX,MAAQA,IAAIoB,GAAG,CAACC,MAAM,CAACA,MAAM,CAAE5B,UAAU,CAAC,iBAAiBO,GAAG,CAACA,IAAIQ,EAAE;QAEjF,OAAO;YACLgB;YACAQ,MAAMrC,KAAKgB,GAAG,CAAC,CAACX;oBACqBA,WAAkB+B;uBAD9B,aACpBlD,wBAAwBmB,IAAIQ,EAAE,GAAER,YAAAA,IAAIY,IAAI,cAARZ,YAAc,CAAC,IAAG+B,eAAAA,QAAQ9B,GAAG,CAACD,IAAIQ,EAAE,aAAlBuB,eAAuB,MAAMD;oBAClFG,OAAOd,MAAMnB;;;YAEfM;QACF;IACF;IACA,MAAM4B,UAAU,MAAMC,eACpBrB,IACAnB,KAAKgB,GAAG,CAAC,CAACX,MAAS,CAAA;YAAEoC,KAAKpC,IAAIoB,GAAG,CAACS,IAAI;YAAErC,QAAQQ,IAAIoB,GAAG,CAACC,MAAM,CAACA,MAAM;YAAGb,IAAIR,IAAIQ,EAAE;YAAEgB,MAAMxB,IAAIC,GAAG,CAAC;QAAQ,CAAA;IAE5G,OAAO;QACLuB;QACAQ,MAAMrC,KAAKgB,GAAG,CAAC,CAACX;gBACkBA,WAAkBkC;mBAD3B,aACpBpD,qBAAqBkB,IAAIQ,EAAE,GAAER,YAAAA,IAAIY,IAAI,cAARZ,YAAc,CAAC,IAAGkC,eAAAA,QAAQjC,GAAG,CAACD,IAAIoB,GAAG,CAACS,IAAI,aAAxBK,eAA6B,MAAMJ;gBACrFG,OAAOd,MAAMnB;;;QAEfM;IACF;AACF;AAEA,wFAAwF,GACxF,eAAe6B,eACbrB,EAAa,EACbuB,IAA+E;IAE/E,MAAMC,SAASD,KAAKE,OAAO,CAAC,CAACC;QAC3B,MAAM/C,aAAaN,2BAA2BqD,IAAIhB,IAAI;QACtD,OAAO/B,aAAa;YAAC,aAAK+C;gBAAK/C;gBAAY2B,KAAKoB,IAAIhD,MAAM,CAACC,UAAU,CAACA,YAAYO,GAAG,CAACwC,IAAIhC,EAAE;;SAAI,GAAG,EAAE;IACvG;IACA,MAAMiC,MAAM,IAAI/B;IAChB,IAAI,CAAC4B,OAAO/B,MAAM,EAAE,OAAOkC;IAC3B,8CAA8C;IAC9C,MAAMzB,YAAY,MAAMF,GAAGG,MAAM,IAAIqB,OAAO3B,GAAG,CAAC,CAAC6B,MAAQA,IAAIpB,GAAG;IAChEkB,OAAOI,OAAO,CAAC,CAACF,KAAKG;YAEcvC;QADjC,MAAMA,WAAWY,SAAS,CAAC2B,MAAM;QACjC,MAAM/B,OAAOR,CAAAA,4BAAAA,SAAUF,MAAM,KAAIE,iBAAAA,SAASQ,IAAI,cAAbR,iBAAmB,CAAC,IAAK;QAC1DqC,IAAIG,GAAG,CAACJ,IAAIJ,GAAG,EAAEI,IAAI/C,UAAU,KAAK,eAAeP,qBAAqB0B,QAAQ3B,iBAAiB2B;IACnG;IACA,OAAO6B;AACT;AAEA,0EAA0E,GAC1E,OAAO,eAAeI,yBACpB/B,EAAa,EACbmB,KAAa,EACbT,IAA6B,EAC7B9B,KAAoB;IAEpB,MAAMF,SAASsB,GAAGrB,UAAU,CAAC,QAAQO,GAAG,CAACiC;IACzC,IAAIT,SAAS,UAAU;QACrB,MAAM,CAAC,EAAE7B,IAAI,EAAEW,IAAI,EAAE,EAAEwC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAACzD,SAASC,QAAQ,mBAAmBE;YAAQe,YAAYjB;SAAQ;QACrH,iGAAiG;QACjG,MAAMuC,UAAU,MAAMlB,QAAQC,IAAInB,KAAKgB,GAAG,CAAC,CAACX,MAAQR,OAAOC,UAAU,CAAC,iBAAiBO,GAAG,CAACA,IAAIQ,EAAE;QACjG,OAAO;YACLgB;YACAQ,MAAMrC,KAAKgB,GAAG,CAAC,CAACX;oBACqBA,WAAkB+B;uBAD9B,aACpBlD,wBAAwBmB,IAAIQ,EAAE,GAAER,YAAAA,IAAIY,IAAI,cAARZ,YAAc,CAAC,IAAG+B,eAAAA,QAAQ9B,GAAG,CAACD,IAAIQ,EAAE,aAAlBuB,eAAuB,MAAMe;oBAClFb;;;YAEF3B;QACF;IACF;IACA,MAAM,CAAC,EAAEX,IAAI,EAAEW,IAAI,EAAE,EAAEwC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAACzD,SAASC,QAAQ,UAAUE;QAAQe,YAAYjB;KAAQ;IAC5G,MAAMyD,aAAatD,KAAKuB,MAAM,CAAC,CAAClB,MAAQb,2BAA2Ba,IAAIC,GAAG,CAAC,aAAa,cAAcU,GAAG,CAAC,CAACX,MAAQA,IAAIQ,EAAE;IACzH,MAAM0C,SAASvD,KAAKuB,MAAM,CAAC,CAAClB,MAAQb,2BAA2Ba,IAAIC,GAAG,CAAC,aAAa,gBAAgBU,GAAG,CAAC,CAACX,MAAQA,IAAIQ,EAAE;IACvH,MAAM,CAAC2C,UAAUC,IAAI,GAAG,MAAML,QAAQC,GAAG,CAAC;QACxCnC,QAAQC,IAAImC,WAAWtC,GAAG,CAAC,CAACH,KAAOhB,OAAOC,UAAU,CAAC,cAAcO,GAAG,CAACQ;QACvEK,QAAQC,IAAIoC,OAAOvC,GAAG,CAAC,CAACH,KAAOhB,OAAOC,UAAU,CAAC,gBAAgBO,GAAG,CAACQ;KACtE;IACD,OAAO;QACLgB;QACAQ,MAAMrC,KAAKgB,GAAG,CAAC,CAACX;gBAIamD,eAEFC,UAEgBpD;YAPzC,MAAMP,aAAaN,2BAA2Ba,IAAIC,GAAG,CAAC;YACtD,MAAMoD,SACJ5D,eAAe,eACXP,sBAAqBiE,gBAAAA,SAASlD,GAAG,CAACD,IAAIQ,EAAE,aAAnB2C,gBAAwB,QAC7C1D,eAAe,iBACbR,kBAAiBmE,WAAAA,IAAInD,GAAG,CAACD,IAAIQ,EAAE,aAAd4C,WAAmB,QACpC;YACR,OAAO,aAAKtE,qBAAqBkB,IAAIQ,EAAE,GAAER,YAAAA,IAAIY,IAAI,cAARZ,YAAc,CAAC,GAAGqD,QAAQP;gBAAUb;;QAC/E;QACA3B;IACF;AACF;AAEA,kFAAkF,GAClF,OAAO,eAAegD,eACpBxC,EAAa,EACbmB,KAAa,EACbsB,KAAa;QAQ8BvD,WAAkBkC;IAN7D,MAAM1C,SAASsB,GAAGrB,UAAU,CAAC,QAAQO,GAAG,CAACiC;IACzC,MAAM,CAACjC,KAAK8C,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAACxD,OAAOC,UAAU,CAAC,UAAUO,GAAG,CAACuD,OAAOtD,GAAG;QAAIQ,YAAYjB;KAAQ;IAC5G,IAAI,CAACQ,IAAIE,MAAM,EAAE,OAAO;QAAEsB,MAAM;QAAQQ,MAAM,EAAE;QAAE1B,MAAM;IAAK;IAC7D,MAAM4B,UAAU,MAAMC,eAAerB,IAAI;QAAC;YAAEsB,KAAKmB;YAAO/D;YAAQgB,IAAI+C;YAAO/B,MAAMxB,IAAIC,GAAG,CAAC;QAAQ;KAAE;IACnG,OAAO;QACLuB,MAAM;QACNQ,MAAM;YAAC,aAAKlD,qBAAqBkB,IAAIQ,EAAE,GAAER,YAAAA,IAAIY,IAAI,cAARZ,YAAc,CAAC,IAAGkC,eAAAA,QAAQjC,GAAG,CAACsD,kBAAZrB,eAAsB,MAAMY;gBAAUb;;SAAQ;QACzG3B,MAAM;IACR;AACF;AAEA,eAAekD,QAAQC,OAAgB;QAMfC,wBAGD9D,cAGFA;IAXnB,MAAM,EAAE+D,MAAM,EAAE/D,KAAK,EAAE8D,SAASE,UAAU,EAAE,GAAG,MAAMpF,qBAAqBiF;IAC1E,MAAMC,UAAUE;IAChB,IAAID,WAAW,OAAO;QACpB,OAAOE,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAqB,GAAG;YAAEC,QAAQ;QAAI;IACtE;IACA,MAAMC,iBAAgBP,yBAAAA,QAAQO,aAAa,YAArBP,yBAAyB;IAC/C,MAAMQ,UAAUD,cAAcE,UAAU,CAAC,aAAaF,cAAc5D,KAAK,CAAC,UAAUE,MAAM,IAAI6D;IAC9F,IAAI,CAACF,SAAS,OAAOL,SAASC,IAAI,CAAC;QAAEC,OAAO;IAAkB,GAAG;QAAEC,QAAQ;IAAI;IAC/E,MAAM/B,QAAQoC,QAAOzE,eAAAA,MAAMqC,KAAK,YAAXrC,eAAe;IACpC,mEAAmE;IACnE,uDAAuD;IACvD,MAAM2B,MAAM8C,QAAOzE,aAAAA,MAAM2B,GAAG,YAAT3B,aAAa;IAChC,IAAI,CAACqC,UAAU,CAACV,KAAK,OAAOsC,SAASC,IAAI,CAAC;QAAEC,OAAO;IAAoB,GAAG;QAAEC,QAAQ;IAAI;IACxF,0EAA0E;IAC1E,MAAMT,QAAQ,OAAO3D,MAAM2D,KAAK,KAAK,YAAY3D,MAAM2D,KAAK,GAAG3D,MAAM2D,KAAK,GAAG;IAC7E,MAAM/B,OAAO5B,MAAM4B,IAAI;IACvB,IAAI,CAACzC,0BAA0ByC,OAAO;QACpC,OAAOqC,SAASC,IAAI,CAAC;YAAEC,OAAO;QAA8B,GAAG;YAAEC,QAAQ;QAAI;IAC/E;IACA,MAAMtE,QAAQ,OAAOE,MAAMF,KAAK,KAAK,YAAYE,MAAMF,KAAK,GAAGE,MAAMF,KAAK,GAAG;IAE7E,IAAI;QACF,MAAM4E,UAAU,MAAM5F,cAAc6F,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACP;QAC/D,IAAI,CAACI,QAAQI,cAAc,IAAI,CAAC/F,uBAAuB2F,UAAU;YAC/D,OAAO7F;QACT;QACA,IAAI,CAAC6F,OAAO,CAAC,QAAQ,EAAE;YACrB,OAAOT,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAa,GAAG;gBAAEC,QAAQ;YAAI;QAC9D;QACA,MAAMlD,KAAKpC,cAAc6F,GAAG,GAAGI,SAAS;QACxC,IAAIpD,KAAK;YACP,MAAMqD,OAAO,MAAMtD,2BAA2BR,IAAIS,KAAKC,MAAM9B;YAC7D,mEAAmE;YACnE,+DAA+D;YAC/D,MAAMd,cAAckC,IAAI;gBACtB+D,UAAUP,QAAQ/C,GAAG;gBACrBuD,QAAQxF;gBACRyF,QAAQ,CAAC,MAAM,EAAExD,KAAK;gBACtByD,YAAYzD;gBACZ0D,QAAQ;gBACRvF,OAAO;oBAAE8B;oBAAMQ,MAAM4C,KAAK5C,IAAI,CAACzB,MAAM;oBAAEb;gBAAM;YAC/C;YACA,OAAOmE,SAASC,IAAI,CAACc,MAAM;gBAAElB,SAAS;oBAAE,iBAAiB;gBAAW;YAAE;QACxE;QACA,IAAI,CAAC,AAAC,CAAA,MAAM5C,GAAGrB,UAAU,CAAC,QAAQO,GAAG,CAACiC,OAAOhC,GAAG,EAAC,EAAGC,MAAM,EAAE;YAC1D,OAAO2D,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAuB,GAAG;gBAAEC,QAAQ;YAAI;QACxE;QACA,MAAMY,OAAOrB,QACT,MAAMD,eAAexC,IAAImB,OAAOsB,SAChC,MAAMV,yBAAyB/B,IAAImB,OAAOT,MAAM9B;QACpD,8EAA8E;QAC9E,MAAMd,cAAckC,IAAI;YACtB+D,UAAUP,QAAQ/C,GAAG;YACrBuD,QAAQzF;YACR0F,QAAQ,CAAC,KAAK,EAAE9C,OAAO;YACvBgD,QAAQ;YACRvF,OAAO;gBAAE8B,MAAM+B,QAAQ,SAAS/B;gBAAMQ,MAAM4C,KAAK5C,IAAI,CAACzB,MAAM;gBAAEb;eAAW6D,QAAQ;gBAAEA;YAAM,IAAI,CAAC;QAChG;QACA,OAAOM,SAASC,IAAI,CAACc,MAAM;YAAElB,SAAS;gBAAE,iBAAiB;YAAW;QAAE;IACxE,EAAE,OAAOK,OAAO;QACdmB,QAAQnB,KAAK,CAAC,4BAA4BA;QAC1C,OAAOF,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAuC,GAAG;YAAEC,QAAQ;QAAI;IACxF;AACF;AAEA,SAASR,WAAW2B,GAAG,GAAE"}
|
|
@@ -28,6 +28,8 @@ export interface StaffAiConversationPerson {
|
|
|
28
28
|
}
|
|
29
29
|
export interface StaffAiAssistRow {
|
|
30
30
|
id: string;
|
|
31
|
+
/** The organization the exchange is filed under (AGL-3660). */
|
|
32
|
+
orgId: string | null;
|
|
31
33
|
at: string | null;
|
|
32
34
|
by: StaffAiConversationPerson;
|
|
33
35
|
hostId: string | null;
|
|
@@ -47,6 +49,12 @@ export interface StaffAiJobOutputRow {
|
|
|
47
49
|
resource: string | null;
|
|
48
50
|
hostSubdomain: string | null;
|
|
49
51
|
}
|
|
52
|
+
/** One field a job's creator filled in, as staff read it. */
|
|
53
|
+
export interface StaffAiJobInput {
|
|
54
|
+
key: string;
|
|
55
|
+
label: string;
|
|
56
|
+
value: string;
|
|
57
|
+
}
|
|
50
58
|
/** A result kept beside the job rather than as a draft. */
|
|
51
59
|
export type StaffAiJobResult = {
|
|
52
60
|
kind: 'insight';
|
|
@@ -67,12 +75,19 @@ export type StaffAiJobResult = {
|
|
|
67
75
|
};
|
|
68
76
|
export interface StaffAiJobRow {
|
|
69
77
|
id: string;
|
|
78
|
+
/** The organization the job is filed under (AGL-3660). */
|
|
79
|
+
orgId: string | null;
|
|
70
80
|
at: string | null;
|
|
71
81
|
by: StaffAiConversationPerson;
|
|
72
82
|
hostId: string | null;
|
|
73
83
|
kind: string;
|
|
74
84
|
status: string;
|
|
75
85
|
brief: string;
|
|
86
|
+
/**
|
|
87
|
+
* What the person filled in beside the brief (AGL-3660) — the guided
|
|
88
|
+
* start's form fields, a tone, a page to edit — as label and value.
|
|
89
|
+
*/
|
|
90
|
+
inputs: StaffAiJobInput[];
|
|
76
91
|
credits: number;
|
|
77
92
|
error: string | null;
|
|
78
93
|
outputs: StaffAiJobOutputRow[];
|
|
@@ -98,6 +113,14 @@ export declare function staffAiInsightResult(record: Data | null): StaffAiJobRes
|
|
|
98
113
|
export declare function staffAiCrmResult(record: Data | null): StaffAiJobResult | null;
|
|
99
114
|
/** The collection a job kind keeps its result beside the job in, or `null` for a kind whose result is a draft. */
|
|
100
115
|
export declare function staffAiJobResultCollection(kind: unknown): 'aiInsights' | 'aiCrmAnswers' | null;
|
|
116
|
+
/** Fields read off one job's inputs, at most. */
|
|
117
|
+
export declare const STAFF_AI_JOB_INPUTS_MAX = 40;
|
|
118
|
+
/**
|
|
119
|
+
* What a job's creator filled in beside the brief (AGL-3660), as fields:
|
|
120
|
+
* scalars and lists as written, one level of a nested object flattened to
|
|
121
|
+
* `parent child`. A value is cut at {@link INPUT_VALUE_MAX} characters.
|
|
122
|
+
*/
|
|
123
|
+
export declare function staffAiJobInputs(inputs: unknown): StaffAiJobInput[];
|
|
101
124
|
/** One job, with the result kept beside it when its kind keeps one. */
|
|
102
125
|
export declare function composeStaffAiJobRow(id: string, job: Data, result: StaffAiJobResult | null, members: ReadonlyMap<string, Data>): StaffAiJobRow;
|
|
103
126
|
export {};
|
|
@@ -70,6 +70,7 @@ const finite = (value)=>typeof value === 'number' && Number.isFinite(value) ? va
|
|
|
70
70
|
const feedback = signal == null ? void 0 : signal['feedback'];
|
|
71
71
|
return {
|
|
72
72
|
id,
|
|
73
|
+
orgId: textOrNull(exchange['orgId']),
|
|
73
74
|
at: staffAiConversationTime(exchange['createdAt']),
|
|
74
75
|
by: staffAiConversationPerson(exchange['uid'], members),
|
|
75
76
|
hostId: textOrNull(exchange['hostId']),
|
|
@@ -126,17 +127,61 @@ const finite = (value)=>typeof value === 'number' && Number.isFinite(value) ? va
|
|
|
126
127
|
if (kind === 'crm') return 'aiCrmAnswers';
|
|
127
128
|
return null;
|
|
128
129
|
}
|
|
130
|
+
/** Fields read off one job's inputs, at most. */ export const STAFF_AI_JOB_INPUTS_MAX = 40;
|
|
131
|
+
/** Characters of one field's value shown. */ const INPUT_VALUE_MAX = 2000;
|
|
132
|
+
/** `businessName` → `Business name`. */ function inputLabel(key) {
|
|
133
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_.-]+/g, ' ').trim().toLowerCase();
|
|
134
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
135
|
+
}
|
|
136
|
+
function inputValue(value) {
|
|
137
|
+
if (value === null || value === undefined) return null;
|
|
138
|
+
if (typeof value === 'string') return value.trim() ? value : null;
|
|
139
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
const parts = value.map(inputValue).filter((part)=>Boolean(part));
|
|
142
|
+
return parts.length ? parts.join(', ') : null;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* What a job's creator filled in beside the brief (AGL-3660), as fields:
|
|
148
|
+
* scalars and lists as written, one level of a nested object flattened to
|
|
149
|
+
* `parent child`. A value is cut at {@link INPUT_VALUE_MAX} characters.
|
|
150
|
+
*/ export function staffAiJobInputs(inputs) {
|
|
151
|
+
if (!inputs || typeof inputs !== 'object' || Array.isArray(inputs)) return [];
|
|
152
|
+
const out = [];
|
|
153
|
+
const push = (key, raw)=>{
|
|
154
|
+
if (out.length >= STAFF_AI_JOB_INPUTS_MAX) return;
|
|
155
|
+
const value = inputValue(raw);
|
|
156
|
+
if (value === null) return;
|
|
157
|
+
out.push({
|
|
158
|
+
key,
|
|
159
|
+
label: inputLabel(key),
|
|
160
|
+
value: value.length > INPUT_VALUE_MAX ? `${value.slice(0, INPUT_VALUE_MAX)}…` : value
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
for (const [key, raw] of Object.entries(inputs)){
|
|
164
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
165
|
+
for (const [child, nested] of Object.entries(raw))push(`${key}.${child}`, nested);
|
|
166
|
+
} else {
|
|
167
|
+
push(key, raw);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
129
172
|
/** One job, with the result kept beside it when its kind keeps one. */ export function composeStaffAiJobRow(id, job, result, members) {
|
|
130
173
|
var _finite;
|
|
131
174
|
const outputs = Array.isArray(job['outputs']) ? job['outputs'] : [];
|
|
132
175
|
return {
|
|
133
176
|
id,
|
|
177
|
+
orgId: textOrNull(job['orgId']),
|
|
134
178
|
at: staffAiConversationTime(job['createdAt']),
|
|
135
179
|
by: staffAiConversationPerson(job['createdBy'], members),
|
|
136
180
|
hostId: textOrNull(job['hostId']),
|
|
137
181
|
kind: text(job['kind']) || 'unknown',
|
|
138
182
|
status: text(job['status']) || 'unknown',
|
|
139
183
|
brief: text(job['brief']),
|
|
184
|
+
inputs: staffAiJobInputs(job['inputs']),
|
|
140
185
|
credits: (_finite = finite(job['creditsSpent'])) != null ? _finite : 0,
|
|
141
186
|
error: textOrNull(job['error']),
|
|
142
187
|
outputs: outputs.map((output)=>({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/staff-org-ai-conversations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT A WORKSPACE ASKED AGLYN AI, FOR STAFF (AGL-3675).\n *\n * The staff org page showed what an org's AI cost and how many jobs ran,\n * never what anybody asked or what came back. Those words are kept — an\n * Assist exchange for 180 days, a job's brief and result for as long as the\n * job — and the Privacy policy says they are accessible to us, but nothing\n * read them, so a stalled sign-up's question could only be found in the\n * database by hand.\n *\n * These are the rows the staff card lists, composed from the stored\n * documents by pure functions so the shape is tested without Firestore:\n *\n * - an Assist row joins `assistExchanges/{id}` (the question, the answer,\n * who asked) with `assistSignals/{id}` (the console route, the model, the\n * cost and the thumbs), which share the id by design;\n * - a job row reads `aiJobs/{id}` (the brief, the kind, the status, the\n * credits, the outputs) and, for the kinds whose result is kept beside\n * the job rather than as a draft, that result: an insight's answer or a\n * CRM answer.\n *\n * The text is shown to the staff member who asked for it and goes nowhere\n * else. The audit row the route writes says that it was read, never what.\n *==========================================*/\n\n/** Rows a page lists; the route asks for one more to learn whether there is a next page. */\nexport const STAFF_AI_CONVERSATIONS_PAGE = 25\n\n/** The two lists the card switches between. */\nexport const STAFF_AI_CONVERSATION_KINDS = ['assist', 'jobs'] as const\nexport type StaffAiConversationKind = (typeof STAFF_AI_CONVERSATION_KINDS)[number]\n\nexport function isStaffAiConversationKind(value: unknown): value is StaffAiConversationKind {\n return typeof value === 'string' && (STAFF_AI_CONVERSATION_KINDS as readonly string[]).includes(value)\n}\n\n/** Who a row is from, as the org's member list names them. */\nexport interface StaffAiConversationPerson {\n uid: string | null\n email: string | null\n name: string | null\n}\n\nexport interface StaffAiAssistRow {\n id: string\n at: string | null\n by: StaffAiConversationPerson\n hostId: string | null\n /** The console route the question was asked from; `null` once the signal is gone or never said. */\n route: string | null\n question: string\n answer: string\n model: string | null\n estCostUsd: number | null\n /** The thumbs the person gave: `up`, `down` or `null`. */\n feedback: 'up' | 'down' | null\n}\n\n/** One thing a job made, as its output names it. */\nexport interface StaffAiJobOutputRow {\n label: string\n note: string | null\n resource: string | null\n hostSubdomain: string | null\n}\n\n/** A result kept beside the job rather than as a draft. */\nexport type StaffAiJobResult =\n | { kind: 'insight'; insights: string[]; gap: string | null }\n | { kind: 'crm-record'; summary: string; nextStep: string | null; standing: string | null }\n | { kind: 'crm-email'; subject: string; body: string }\n | { kind: 'crm-mapping'; matches: string[] }\n\nexport interface StaffAiJobRow {\n id: string\n at: string | null\n by: StaffAiConversationPerson\n hostId: string | null\n kind: string\n status: string\n brief: string\n credits: number\n error: string | null\n outputs: StaffAiJobOutputRow[]\n /** `null` when the kind keeps nothing beside the job, or the result has expired. */\n result: StaffAiJobResult | null\n}\n\nexport interface StaffAiConversationsResponse {\n kind: StaffAiConversationKind\n rows: StaffAiAssistRow[] | StaffAiJobRow[]\n /** The id to pass as `after` for the next page; `null` on the last. */\n next: string | null\n}\n\ntype Data = Record<string, unknown>\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\nconst textOrNull = (value: unknown): string | null => (typeof value === 'string' && value.trim() ? value : null)\nconst finite = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\n\n/** A stored time as ISO: a Firestore timestamp, a `Date`, milliseconds or an ISO string. */\nexport function staffAiConversationTime(value: unknown): string | null {\n if (value == null) return null\n if (typeof (value as { toDate?: unknown }).toDate === 'function') {\n return (value as { toDate: () => Date }).toDate().toISOString()\n }\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString()\n if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString()\n if (typeof value === 'string' && !Number.isNaN(Date.parse(value))) return new Date(value).toISOString()\n return null\n}\n\n/** The person a uid is, from `orgs/{org}/members/{uid}` documents keyed by uid. */\nexport function staffAiConversationPerson(\n uid: unknown,\n members: ReadonlyMap<string, Data>,\n): StaffAiConversationPerson {\n const id = typeof uid === 'string' && uid ? uid : null\n const member = id ? members.get(id) : undefined\n return {\n uid: id,\n email: textOrNull(member?.['email']),\n name: textOrNull(member?.['displayName']),\n }\n}\n\n/** One Assist exchange joined with its signal, which may already be gone or never have been written. */\nexport function composeStaffAiAssistRow(\n id: string,\n exchange: Data,\n signal: Data | null,\n members: ReadonlyMap<string, Data>,\n): StaffAiAssistRow {\n const feedback = signal?.['feedback']\n return {\n id,\n at: staffAiConversationTime(exchange['createdAt']),\n by: staffAiConversationPerson(exchange['uid'], members),\n hostId: textOrNull(exchange['hostId']),\n route: textOrNull(signal?.['route']),\n question: text(exchange['question']),\n answer: text(exchange['answer']),\n model: textOrNull(signal?.['model']),\n estCostUsd: finite(signal?.['estCostUsd']),\n feedback: feedback === 'up' || feedback === 'down' ? feedback : null,\n }\n}\n\n/** An insight's kept answer, as `aiInsights/{jobId}` stores it. */\nexport function staffAiInsightResult(record: Data | null): StaffAiJobResult | null {\n if (!record) return null\n const insights = Array.isArray(record['insights'])\n ? (record['insights'] as Data[]).map((insight) => text(insight?.['text'])).filter(Boolean)\n : []\n return { kind: 'insight', insights, gap: textOrNull(record['gap']) }\n}\n\n/** A CRM answer, as `aiCrmAnswers/{jobId}` stores it, by the proposal it holds. */\nexport function staffAiCrmResult(record: Data | null): StaffAiJobResult | null {\n const proposal = record?.['proposal'] as Data | undefined\n if (!proposal) return null\n switch (proposal['kind']) {\n case 'record': {\n const nextStep = proposal['nextStep'] as Data | null | undefined\n const title = textOrNull(nextStep?.['title'])\n const reason = textOrNull(nextStep?.['reason'])\n return {\n kind: 'crm-record',\n summary: text(proposal['summary']),\n nextStep: title ? (reason ? `${title} — ${reason}` : title) : null,\n standing: textOrNull(proposal['standing']),\n }\n }\n case 'email':\n return { kind: 'crm-email', subject: text(proposal['subject']), body: text(proposal['body']) }\n case 'mapping':\n return {\n kind: 'crm-mapping',\n matches: Array.isArray(proposal['matches'])\n ? (proposal['matches'] as Data[]).map((match) => `${text(match?.['header'])} → ${text(match?.['label'])}`)\n : [],\n }\n default:\n return null\n }\n}\n\n/** The collection a job kind keeps its result beside the job in, or `null` for a kind whose result is a draft. */\nexport function staffAiJobResultCollection(kind: unknown): 'aiInsights' | 'aiCrmAnswers' | null {\n if (kind === 'insight') return 'aiInsights'\n if (kind === 'crm') return 'aiCrmAnswers'\n return null\n}\n\n/** One job, with the result kept beside it when its kind keeps one. */\nexport function composeStaffAiJobRow(\n id: string,\n job: Data,\n result: StaffAiJobResult | null,\n members: ReadonlyMap<string, Data>,\n): StaffAiJobRow {\n const outputs = Array.isArray(job['outputs']) ? (job['outputs'] as Data[]) : []\n return {\n id,\n at: staffAiConversationTime(job['createdAt']),\n by: staffAiConversationPerson(job['createdBy'], members),\n hostId: textOrNull(job['hostId']),\n kind: text(job['kind']) || 'unknown',\n status: text(job['status']) || 'unknown',\n brief: text(job['brief']),\n credits: finite(job['creditsSpent']) ?? 0,\n error: textOrNull(job['error']),\n outputs: outputs.map((output) => ({\n label: text(output?.['label']) || text(output?.['resource']) || 'Output',\n note: textOrNull(output?.['note']),\n resource: textOrNull(output?.['resource']),\n hostSubdomain: textOrNull(output?.['hostSubdomain']),\n })),\n result,\n }\n}\n"],"names":["STAFF_AI_CONVERSATIONS_PAGE","STAFF_AI_CONVERSATION_KINDS","isStaffAiConversationKind","value","includes","text","textOrNull","trim","finite","Number","isFinite","staffAiConversationTime","toDate","toISOString","Date","isNaN","getTime","parse","staffAiConversationPerson","uid","members","id","member","get","undefined","email","name","composeStaffAiAssistRow","exchange","signal","feedback","at","by","hostId","route","question","answer","model","estCostUsd","staffAiInsightResult","record","insights","Array","isArray","map","insight","filter","Boolean","kind","gap","staffAiCrmResult","proposal","nextStep","title","reason","summary","standing","subject","body","matches","match","staffAiJobResultCollection","composeStaffAiJobRow","job","result","outputs","status","brief","credits","error","output","label","note","resource","hostSubdomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;4CAuB4C,GAE5C,0FAA0F,GAC1F,OAAO,MAAMA,8BAA8B,GAAE;AAE7C,6CAA6C,GAC7C,OAAO,MAAMC,8BAA8B;IAAC;IAAU;CAAO,CAAS;AAGtE,OAAO,SAASC,0BAA0BC,KAAc;IACtD,OAAO,OAAOA,UAAU,YAAY,AAACF,4BAAkDG,QAAQ,CAACD;AAClG;AA+DA,MAAME,OAAO,CAACF,QAA4B,OAAOA,UAAU,WAAWA,QAAQ;AAC9E,MAAMG,aAAa,CAACH,QAAmC,OAAOA,UAAU,YAAYA,MAAMI,IAAI,KAAKJ,QAAQ;AAC3G,MAAMK,SAAS,CAACL,QACd,OAAOA,UAAU,YAAYM,OAAOC,QAAQ,CAACP,SAASA,QAAQ;AAEhE,0FAA0F,GAC1F,OAAO,SAASQ,wBAAwBR,KAAc;IACpD,IAAIA,SAAS,MAAM,OAAO;IAC1B,IAAI,OAAO,AAACA,MAA+BS,MAAM,KAAK,YAAY;QAChE,OAAO,AAACT,MAAiCS,MAAM,GAAGC,WAAW;IAC/D;IACA,IAAIV,iBAAiBW,MAAM,OAAOL,OAAOM,KAAK,CAACZ,MAAMa,OAAO,MAAM,OAAOb,MAAMU,WAAW;IAC1F,IAAI,OAAOV,UAAU,YAAYM,OAAOC,QAAQ,CAACP,QAAQ,OAAO,IAAIW,KAAKX,OAAOU,WAAW;IAC3F,IAAI,OAAOV,UAAU,YAAY,CAACM,OAAOM,KAAK,CAACD,KAAKG,KAAK,CAACd,SAAS,OAAO,IAAIW,KAAKX,OAAOU,WAAW;IACrG,OAAO;AACT;AAEA,iFAAiF,GACjF,OAAO,SAASK,0BACdC,GAAY,EACZC,OAAkC;IAElC,MAAMC,KAAK,OAAOF,QAAQ,YAAYA,MAAMA,MAAM;IAClD,MAAMG,SAASD,KAAKD,QAAQG,GAAG,CAACF,MAAMG;IACtC,OAAO;QACLL,KAAKE;QACLI,OAAOnB,WAAWgB,0BAAAA,MAAQ,CAAC,QAAQ;QACnCI,MAAMpB,WAAWgB,0BAAAA,MAAQ,CAAC,cAAc;IAC1C;AACF;AAEA,sGAAsG,GACtG,OAAO,SAASK,wBACdN,EAAU,EACVO,QAAc,EACdC,MAAmB,EACnBT,OAAkC;IAElC,MAAMU,WAAWD,0BAAAA,MAAQ,CAAC,WAAW;IACrC,OAAO;QACLR;QACAU,IAAIpB,wBAAwBiB,QAAQ,CAAC,YAAY;QACjDI,IAAId,0BAA0BU,QAAQ,CAAC,MAAM,EAAER;QAC/Ca,QAAQ3B,WAAWsB,QAAQ,CAAC,SAAS;QACrCM,OAAO5B,WAAWuB,0BAAAA,MAAQ,CAAC,QAAQ;QACnCM,UAAU9B,KAAKuB,QAAQ,CAAC,WAAW;QACnCQ,QAAQ/B,KAAKuB,QAAQ,CAAC,SAAS;QAC/BS,OAAO/B,WAAWuB,0BAAAA,MAAQ,CAAC,QAAQ;QACnCS,YAAY9B,OAAOqB,0BAAAA,MAAQ,CAAC,aAAa;QACzCC,UAAUA,aAAa,QAAQA,aAAa,SAASA,WAAW;IAClE;AACF;AAEA,iEAAiE,GACjE,OAAO,SAASS,qBAAqBC,MAAmB;IACtD,IAAI,CAACA,QAAQ,OAAO;IACpB,MAAMC,WAAWC,MAAMC,OAAO,CAACH,MAAM,CAAC,WAAW,IAC7C,AAACA,MAAM,CAAC,WAAW,CAAYI,GAAG,CAAC,CAACC,UAAYxC,KAAKwC,2BAAAA,OAAS,CAAC,OAAO,GAAGC,MAAM,CAACC,WAChF,EAAE;IACN,OAAO;QAAEC,MAAM;QAAWP;QAAUQ,KAAK3C,WAAWkC,MAAM,CAAC,MAAM;IAAE;AACrE;AAEA,iFAAiF,GACjF,OAAO,SAASU,iBAAiBV,MAAmB;IAClD,MAAMW,WAAWX,0BAAAA,MAAQ,CAAC,WAAW;IACrC,IAAI,CAACW,UAAU,OAAO;IACtB,OAAQA,QAAQ,CAAC,OAAO;QACtB,KAAK;YAAU;gBACb,MAAMC,WAAWD,QAAQ,CAAC,WAAW;gBACrC,MAAME,QAAQ/C,WAAW8C,4BAAAA,QAAU,CAAC,QAAQ;gBAC5C,MAAME,SAAShD,WAAW8C,4BAAAA,QAAU,CAAC,SAAS;gBAC9C,OAAO;oBACLJ,MAAM;oBACNO,SAASlD,KAAK8C,QAAQ,CAAC,UAAU;oBACjCC,UAAUC,QAASC,SAAS,GAAGD,MAAM,GAAG,EAAEC,QAAQ,GAAGD,QAAS;oBAC9DG,UAAUlD,WAAW6C,QAAQ,CAAC,WAAW;gBAC3C;YACF;QACA,KAAK;YACH,OAAO;gBAAEH,MAAM;gBAAaS,SAASpD,KAAK8C,QAAQ,CAAC,UAAU;gBAAGO,MAAMrD,KAAK8C,QAAQ,CAAC,OAAO;YAAE;QAC/F,KAAK;YACH,OAAO;gBACLH,MAAM;gBACNW,SAASjB,MAAMC,OAAO,CAACQ,QAAQ,CAAC,UAAU,IACtC,AAACA,QAAQ,CAAC,UAAU,CAAYP,GAAG,CAAC,CAACgB,QAAU,GAAGvD,KAAKuD,yBAAAA,KAAO,CAAC,SAAS,EAAE,GAAG,EAAEvD,KAAKuD,yBAAAA,KAAO,CAAC,QAAQ,GAAG,IACvG,EAAE;YACR;QACF;YACE,OAAO;IACX;AACF;AAEA,gHAAgH,GAChH,OAAO,SAASC,2BAA2Bb,IAAa;IACtD,IAAIA,SAAS,WAAW,OAAO;IAC/B,IAAIA,SAAS,OAAO,OAAO;IAC3B,OAAO;AACT;AAEA,qEAAqE,GACrE,OAAO,SAASc,qBACdzC,EAAU,EACV0C,GAAS,EACTC,MAA+B,EAC/B5C,OAAkC;QAWvBZ;IATX,MAAMyD,UAAUvB,MAAMC,OAAO,CAACoB,GAAG,CAAC,UAAU,IAAKA,GAAG,CAAC,UAAU,GAAc,EAAE;IAC/E,OAAO;QACL1C;QACAU,IAAIpB,wBAAwBoD,GAAG,CAAC,YAAY;QAC5C/B,IAAId,0BAA0B6C,GAAG,CAAC,YAAY,EAAE3C;QAChDa,QAAQ3B,WAAWyD,GAAG,CAAC,SAAS;QAChCf,MAAM3C,KAAK0D,GAAG,CAAC,OAAO,KAAK;QAC3BG,QAAQ7D,KAAK0D,GAAG,CAAC,SAAS,KAAK;QAC/BI,OAAO9D,KAAK0D,GAAG,CAAC,QAAQ;QACxBK,OAAO,GAAE5D,UAAAA,OAAOuD,GAAG,CAAC,eAAe,aAA1BvD,UAA+B;QACxC6D,OAAO/D,WAAWyD,GAAG,CAAC,QAAQ;QAC9BE,SAASA,QAAQrB,GAAG,CAAC,CAAC0B,SAAY,CAAA;gBAChCC,OAAOlE,KAAKiE,0BAAAA,MAAQ,CAAC,QAAQ,KAAKjE,KAAKiE,0BAAAA,MAAQ,CAAC,WAAW,KAAK;gBAChEE,MAAMlE,WAAWgE,0BAAAA,MAAQ,CAAC,OAAO;gBACjCG,UAAUnE,WAAWgE,0BAAAA,MAAQ,CAAC,WAAW;gBACzCI,eAAepE,WAAWgE,0BAAAA,MAAQ,CAAC,gBAAgB;YACrD,CAAA;QACAN;IACF;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/staff-org-ai-conversations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT A WORKSPACE ASKED AGLYN AI, FOR STAFF (AGL-3675).\n *\n * The staff org page showed what an org's AI cost and how many jobs ran,\n * never what anybody asked or what came back. Those words are kept — an\n * Assist exchange for 180 days, a job's brief and result for as long as the\n * job — and the Privacy policy says they are accessible to us, but nothing\n * read them, so a stalled sign-up's question could only be found in the\n * database by hand.\n *\n * These are the rows the staff card lists, composed from the stored\n * documents by pure functions so the shape is tested without Firestore:\n *\n * - an Assist row joins `assistExchanges/{id}` (the question, the answer,\n * who asked) with `assistSignals/{id}` (the console route, the model, the\n * cost and the thumbs), which share the id by design;\n * - a job row reads `aiJobs/{id}` (the brief, the kind, the status, the\n * credits, the outputs) and, for the kinds whose result is kept beside\n * the job rather than as a draft, that result: an insight's answer or a\n * CRM answer.\n *\n * The text is shown to the staff member who asked for it and goes nowhere\n * else. The audit row the route writes says that it was read, never what.\n *==========================================*/\n\n/** Rows a page lists; the route asks for one more to learn whether there is a next page. */\nexport const STAFF_AI_CONVERSATIONS_PAGE = 25\n\n/** The two lists the card switches between. */\nexport const STAFF_AI_CONVERSATION_KINDS = ['assist', 'jobs'] as const\nexport type StaffAiConversationKind = (typeof STAFF_AI_CONVERSATION_KINDS)[number]\n\nexport function isStaffAiConversationKind(value: unknown): value is StaffAiConversationKind {\n return typeof value === 'string' && (STAFF_AI_CONVERSATION_KINDS as readonly string[]).includes(value)\n}\n\n/** Who a row is from, as the org's member list names them. */\nexport interface StaffAiConversationPerson {\n uid: string | null\n email: string | null\n name: string | null\n}\n\nexport interface StaffAiAssistRow {\n id: string\n /** The organization the exchange is filed under (AGL-3660). */\n orgId: string | null\n at: string | null\n by: StaffAiConversationPerson\n hostId: string | null\n /** The console route the question was asked from; `null` once the signal is gone or never said. */\n route: string | null\n question: string\n answer: string\n model: string | null\n estCostUsd: number | null\n /** The thumbs the person gave: `up`, `down` or `null`. */\n feedback: 'up' | 'down' | null\n}\n\n/** One thing a job made, as its output names it. */\nexport interface StaffAiJobOutputRow {\n label: string\n note: string | null\n resource: string | null\n hostSubdomain: string | null\n}\n\n/** One field a job's creator filled in, as staff read it. */\nexport interface StaffAiJobInput {\n key: string\n label: string\n value: string\n}\n\n/** A result kept beside the job rather than as a draft. */\nexport type StaffAiJobResult =\n | { kind: 'insight'; insights: string[]; gap: string | null }\n | { kind: 'crm-record'; summary: string; nextStep: string | null; standing: string | null }\n | { kind: 'crm-email'; subject: string; body: string }\n | { kind: 'crm-mapping'; matches: string[] }\n\nexport interface StaffAiJobRow {\n id: string\n /** The organization the job is filed under (AGL-3660). */\n orgId: string | null\n at: string | null\n by: StaffAiConversationPerson\n hostId: string | null\n kind: string\n status: string\n brief: string\n /**\n * What the person filled in beside the brief (AGL-3660) — the guided\n * start's form fields, a tone, a page to edit — as label and value.\n */\n inputs: StaffAiJobInput[]\n credits: number\n error: string | null\n outputs: StaffAiJobOutputRow[]\n /** `null` when the kind keeps nothing beside the job, or the result has expired. */\n result: StaffAiJobResult | null\n}\n\nexport interface StaffAiConversationsResponse {\n kind: StaffAiConversationKind\n rows: StaffAiAssistRow[] | StaffAiJobRow[]\n /** The id to pass as `after` for the next page; `null` on the last. */\n next: string | null\n}\n\ntype Data = Record<string, unknown>\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\nconst textOrNull = (value: unknown): string | null => (typeof value === 'string' && value.trim() ? value : null)\nconst finite = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) ? value : null\n\n/** A stored time as ISO: a Firestore timestamp, a `Date`, milliseconds or an ISO string. */\nexport function staffAiConversationTime(value: unknown): string | null {\n if (value == null) return null\n if (typeof (value as { toDate?: unknown }).toDate === 'function') {\n return (value as { toDate: () => Date }).toDate().toISOString()\n }\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString()\n if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString()\n if (typeof value === 'string' && !Number.isNaN(Date.parse(value))) return new Date(value).toISOString()\n return null\n}\n\n/** The person a uid is, from `orgs/{org}/members/{uid}` documents keyed by uid. */\nexport function staffAiConversationPerson(\n uid: unknown,\n members: ReadonlyMap<string, Data>,\n): StaffAiConversationPerson {\n const id = typeof uid === 'string' && uid ? uid : null\n const member = id ? members.get(id) : undefined\n return {\n uid: id,\n email: textOrNull(member?.['email']),\n name: textOrNull(member?.['displayName']),\n }\n}\n\n/** One Assist exchange joined with its signal, which may already be gone or never have been written. */\nexport function composeStaffAiAssistRow(\n id: string,\n exchange: Data,\n signal: Data | null,\n members: ReadonlyMap<string, Data>,\n): StaffAiAssistRow {\n const feedback = signal?.['feedback']\n return {\n id,\n orgId: textOrNull(exchange['orgId']),\n at: staffAiConversationTime(exchange['createdAt']),\n by: staffAiConversationPerson(exchange['uid'], members),\n hostId: textOrNull(exchange['hostId']),\n route: textOrNull(signal?.['route']),\n question: text(exchange['question']),\n answer: text(exchange['answer']),\n model: textOrNull(signal?.['model']),\n estCostUsd: finite(signal?.['estCostUsd']),\n feedback: feedback === 'up' || feedback === 'down' ? feedback : null,\n }\n}\n\n/** An insight's kept answer, as `aiInsights/{jobId}` stores it. */\nexport function staffAiInsightResult(record: Data | null): StaffAiJobResult | null {\n if (!record) return null\n const insights = Array.isArray(record['insights'])\n ? (record['insights'] as Data[]).map((insight) => text(insight?.['text'])).filter(Boolean)\n : []\n return { kind: 'insight', insights, gap: textOrNull(record['gap']) }\n}\n\n/** A CRM answer, as `aiCrmAnswers/{jobId}` stores it, by the proposal it holds. */\nexport function staffAiCrmResult(record: Data | null): StaffAiJobResult | null {\n const proposal = record?.['proposal'] as Data | undefined\n if (!proposal) return null\n switch (proposal['kind']) {\n case 'record': {\n const nextStep = proposal['nextStep'] as Data | null | undefined\n const title = textOrNull(nextStep?.['title'])\n const reason = textOrNull(nextStep?.['reason'])\n return {\n kind: 'crm-record',\n summary: text(proposal['summary']),\n nextStep: title ? (reason ? `${title} — ${reason}` : title) : null,\n standing: textOrNull(proposal['standing']),\n }\n }\n case 'email':\n return { kind: 'crm-email', subject: text(proposal['subject']), body: text(proposal['body']) }\n case 'mapping':\n return {\n kind: 'crm-mapping',\n matches: Array.isArray(proposal['matches'])\n ? (proposal['matches'] as Data[]).map((match) => `${text(match?.['header'])} → ${text(match?.['label'])}`)\n : [],\n }\n default:\n return null\n }\n}\n\n/** The collection a job kind keeps its result beside the job in, or `null` for a kind whose result is a draft. */\nexport function staffAiJobResultCollection(kind: unknown): 'aiInsights' | 'aiCrmAnswers' | null {\n if (kind === 'insight') return 'aiInsights'\n if (kind === 'crm') return 'aiCrmAnswers'\n return null\n}\n\n/** Fields read off one job's inputs, at most. */\nexport const STAFF_AI_JOB_INPUTS_MAX = 40\n/** Characters of one field's value shown. */\nconst INPUT_VALUE_MAX = 2000\n\n/** `businessName` → `Business name`. */\nfunction inputLabel(key: string): string {\n const words = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_.-]+/g, ' ')\n .trim()\n .toLowerCase()\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\nfunction inputValue(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') return value.trim() ? value : null\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (Array.isArray(value)) {\n const parts = value.map(inputValue).filter((part): part is string => Boolean(part))\n return parts.length ? parts.join(', ') : null\n }\n return null\n}\n\n/**\n * What a job's creator filled in beside the brief (AGL-3660), as fields:\n * scalars and lists as written, one level of a nested object flattened to\n * `parent child`. A value is cut at {@link INPUT_VALUE_MAX} characters.\n */\nexport function staffAiJobInputs(inputs: unknown): StaffAiJobInput[] {\n if (!inputs || typeof inputs !== 'object' || Array.isArray(inputs)) return []\n const out: StaffAiJobInput[] = []\n const push = (key: string, raw: unknown) => {\n if (out.length >= STAFF_AI_JOB_INPUTS_MAX) return\n const value = inputValue(raw)\n if (value === null) return\n out.push({\n key,\n label: inputLabel(key),\n value: value.length > INPUT_VALUE_MAX ? `${value.slice(0, INPUT_VALUE_MAX)}…` : value,\n })\n }\n for (const [key, raw] of Object.entries(inputs as Data)) {\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n for (const [child, nested] of Object.entries(raw as Data)) push(`${key}.${child}`, nested)\n } else {\n push(key, raw)\n }\n }\n return out\n}\n\n/** One job, with the result kept beside it when its kind keeps one. */\nexport function composeStaffAiJobRow(\n id: string,\n job: Data,\n result: StaffAiJobResult | null,\n members: ReadonlyMap<string, Data>,\n): StaffAiJobRow {\n const outputs = Array.isArray(job['outputs']) ? (job['outputs'] as Data[]) : []\n return {\n id,\n orgId: textOrNull(job['orgId']),\n at: staffAiConversationTime(job['createdAt']),\n by: staffAiConversationPerson(job['createdBy'], members),\n hostId: textOrNull(job['hostId']),\n kind: text(job['kind']) || 'unknown',\n status: text(job['status']) || 'unknown',\n brief: text(job['brief']),\n inputs: staffAiJobInputs(job['inputs']),\n credits: finite(job['creditsSpent']) ?? 0,\n error: textOrNull(job['error']),\n outputs: outputs.map((output) => ({\n label: text(output?.['label']) || text(output?.['resource']) || 'Output',\n note: textOrNull(output?.['note']),\n resource: textOrNull(output?.['resource']),\n hostSubdomain: textOrNull(output?.['hostSubdomain']),\n })),\n result,\n }\n}\n"],"names":["STAFF_AI_CONVERSATIONS_PAGE","STAFF_AI_CONVERSATION_KINDS","isStaffAiConversationKind","value","includes","text","textOrNull","trim","finite","Number","isFinite","staffAiConversationTime","toDate","toISOString","Date","isNaN","getTime","parse","staffAiConversationPerson","uid","members","id","member","get","undefined","email","name","composeStaffAiAssistRow","exchange","signal","feedback","orgId","at","by","hostId","route","question","answer","model","estCostUsd","staffAiInsightResult","record","insights","Array","isArray","map","insight","filter","Boolean","kind","gap","staffAiCrmResult","proposal","nextStep","title","reason","summary","standing","subject","body","matches","match","staffAiJobResultCollection","STAFF_AI_JOB_INPUTS_MAX","INPUT_VALUE_MAX","inputLabel","key","words","replace","toLowerCase","charAt","toUpperCase","slice","inputValue","String","parts","part","length","join","staffAiJobInputs","inputs","out","push","raw","label","Object","entries","child","nested","composeStaffAiJobRow","job","result","outputs","status","brief","credits","error","output","note","resource","hostSubdomain"],"mappings":"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"}
|