@fayz-ai/admin 0.62.0 → 0.65.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CommandPalette-B5PMQFDG.js → CommandPalette-XDJJ5E3O.js} +3 -3
- package/dist/{CommandPalette-B5PMQFDG.js.map → CommandPalette-XDJJ5E3O.js.map} +1 -1
- package/dist/ConnectedBrandingSettings-43ZM3HCI.js +680 -0
- package/dist/ConnectedBrandingSettings-43ZM3HCI.js.map +1 -0
- package/dist/ConnectedCompanySettings-I4MALI2Q.js +197 -0
- package/dist/ConnectedCompanySettings-I4MALI2Q.js.map +1 -0
- package/dist/ConnectedModulesSettings-A2VJ5BMJ.js +563 -0
- package/dist/ConnectedModulesSettings-A2VJ5BMJ.js.map +1 -0
- package/dist/ConnectedSecuritySettings-25AHA4HJ.js +189 -0
- package/dist/ConnectedSecuritySettings-25AHA4HJ.js.map +1 -0
- package/dist/ConnectedUserProfile-E7USJRUE.js +424 -0
- package/dist/ConnectedUserProfile-E7USJRUE.js.map +1 -0
- package/dist/FieldRulesEditor-YGIPKI5T.js +129 -0
- package/dist/FieldRulesEditor-YGIPKI5T.js.map +1 -0
- package/dist/GettingStartedWidget-HMIJ3Y24.js +12 -0
- package/dist/GettingStartedWidget-HMIJ3Y24.js.map +1 -0
- package/dist/PermissionProfilesTab-3ZPRCHMZ.js +548 -0
- package/dist/PermissionProfilesTab-3ZPRCHMZ.js.map +1 -0
- package/dist/PersonTimeline-HQZT527S.js +3 -0
- package/dist/{PersonTimeline-X7JTPUWJ.js.map → PersonTimeline-HQZT527S.js.map} +1 -1
- package/dist/ProjectsSettings-7O2F27MD.js +5 -0
- package/dist/ProjectsSettings-7O2F27MD.js.map +1 -0
- package/dist/ScheduleEditor-54ELWR45.js +8 -0
- package/dist/{ScheduleEditor-L2UJXKMY.js.map → ScheduleEditor-54ELWR45.js.map} +1 -1
- package/dist/SeuSiteWidget-QLSUVC63.js +6 -0
- package/dist/SeuSiteWidget-QLSUVC63.js.map +1 -0
- package/dist/SubscriptionPage-5GE5HFJK.js +17 -0
- package/dist/{SubscriptionPage-IDVEFLZ5.js.map → SubscriptionPage-5GE5HFJK.js.map} +1 -1
- package/dist/TeamTab-6RAULFLW.js +502 -0
- package/dist/TeamTab-6RAULFLW.js.map +1 -0
- package/dist/TrialWidget-Z6CLUCU3.js +7 -0
- package/dist/TrialWidget-Z6CLUCU3.js.map +1 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +14 -0
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/routing.d.ts +6 -0
- package/dist/app/routing.d.ts.map +1 -1
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/billing/index.js +9 -4
- package/dist/chunk-2H46WDG5.js +53 -0
- package/dist/chunk-2H46WDG5.js.map +1 -0
- package/dist/chunk-3HLX7YB2.js +94 -0
- package/dist/chunk-3HLX7YB2.js.map +1 -0
- package/dist/chunk-3OUD4XBK.js +3 -0
- package/dist/chunk-3OUD4XBK.js.map +1 -0
- package/dist/{chunk-IXSY4GPK.js → chunk-5D5QLMOQ.js} +5 -3
- package/dist/chunk-5D5QLMOQ.js.map +1 -0
- package/dist/chunk-5WR62X2R.js +1081 -0
- package/dist/chunk-5WR62X2R.js.map +1 -0
- package/dist/{chunk-KPYRTOCK.js → chunk-6CQFGMEO.js} +4 -140
- package/dist/chunk-6CQFGMEO.js.map +1 -0
- package/dist/chunk-6MMSYC2I.js +116 -0
- package/dist/chunk-6MMSYC2I.js.map +1 -0
- package/dist/chunk-6QFOE3UD.js +1466 -0
- package/dist/chunk-6QFOE3UD.js.map +1 -0
- package/dist/chunk-6UUFVQ2E.js +283 -0
- package/dist/chunk-6UUFVQ2E.js.map +1 -0
- package/dist/chunk-7WNLL3F6.js +7889 -0
- package/dist/chunk-7WNLL3F6.js.map +1 -0
- package/dist/chunk-A74IHAQE.js +439 -0
- package/dist/chunk-A74IHAQE.js.map +1 -0
- package/dist/{chunk-JOBRLBIQ.js → chunk-AFA6IO23.js} +4 -3
- package/dist/chunk-AFA6IO23.js.map +1 -0
- package/dist/{chunk-P5UIGQFY.js → chunk-ALCSIN3Z.js} +5 -5
- package/dist/chunk-ALCSIN3Z.js.map +1 -0
- package/dist/chunk-AWM4AMBS.js +39 -0
- package/dist/chunk-AWM4AMBS.js.map +1 -0
- package/dist/{chunk-OQARLKZN.js → chunk-BG5Z6KLN.js} +6 -18
- package/dist/chunk-BG5Z6KLN.js.map +1 -0
- package/dist/chunk-DPNFZONC.js +19 -0
- package/dist/chunk-DPNFZONC.js.map +1 -0
- package/dist/chunk-DRCV643L.js +193 -0
- package/dist/chunk-DRCV643L.js.map +1 -0
- package/dist/chunk-E6DF3VR6.js +206 -0
- package/dist/chunk-E6DF3VR6.js.map +1 -0
- package/dist/chunk-ENHCVGXZ.js +120 -0
- package/dist/chunk-ENHCVGXZ.js.map +1 -0
- package/dist/chunk-FK6RHGI7.js +56 -0
- package/dist/chunk-FK6RHGI7.js.map +1 -0
- package/dist/chunk-G5HNA5QK.js +233 -0
- package/dist/chunk-G5HNA5QK.js.map +1 -0
- package/dist/chunk-HCFO4Z6A.js +53 -0
- package/dist/chunk-HCFO4Z6A.js.map +1 -0
- package/dist/chunk-JFIU6NQP.js +51 -0
- package/dist/chunk-JFIU6NQP.js.map +1 -0
- package/dist/chunk-JRZJBMOY.js +3 -0
- package/dist/chunk-JRZJBMOY.js.map +1 -0
- package/dist/{chunk-3YP7KW47.js → chunk-JVVOL4BB.js} +41 -512
- package/dist/chunk-JVVOL4BB.js.map +1 -0
- package/dist/chunk-KAQ3UIAA.js +415 -0
- package/dist/chunk-KAQ3UIAA.js.map +1 -0
- package/dist/{chunk-P6XUDT2P.js → chunk-KJELMULA.js} +4 -51
- package/dist/chunk-KJELMULA.js.map +1 -0
- package/dist/chunk-KLO7DKXX.js +1651 -0
- package/dist/chunk-KLO7DKXX.js.map +1 -0
- package/dist/chunk-M2CMDWUI.js +75 -0
- package/dist/chunk-M2CMDWUI.js.map +1 -0
- package/dist/chunk-PQ47MLTH.js +255 -0
- package/dist/chunk-PQ47MLTH.js.map +1 -0
- package/dist/{chunk-VX3WJLKK.js → chunk-QD7BMUDF.js} +17 -8
- package/dist/chunk-QD7BMUDF.js.map +1 -0
- package/dist/{chunk-E4MESMNR.js → chunk-QTSKCPSW.js} +7 -10
- package/dist/chunk-QTSKCPSW.js.map +1 -0
- package/dist/chunk-S56FQKPL.js +186 -0
- package/dist/chunk-S56FQKPL.js.map +1 -0
- package/dist/chunk-SGAFPNMA.js +75 -0
- package/dist/chunk-SGAFPNMA.js.map +1 -0
- package/dist/{chunk-OD7P4L3D.js → chunk-SOKCLVM4.js} +3 -3
- package/dist/{chunk-OD7P4L3D.js.map → chunk-SOKCLVM4.js.map} +1 -1
- package/dist/chunk-TL42NKTH.js +3 -0
- package/dist/chunk-TL42NKTH.js.map +1 -0
- package/dist/chunk-U6ZCO2PS.js +2964 -0
- package/dist/chunk-U6ZCO2PS.js.map +1 -0
- package/dist/chunk-VSXNROYH.js +16 -0
- package/dist/chunk-VSXNROYH.js.map +1 -0
- package/dist/chunk-WPFZCYZL.js +619 -0
- package/dist/chunk-WPFZCYZL.js.map +1 -0
- package/dist/chunk-XFTNIBDW.js +23 -0
- package/dist/chunk-XFTNIBDW.js.map +1 -0
- package/dist/chunk-Y66JDL6I.js +1020 -0
- package/dist/chunk-Y66JDL6I.js.map +1 -0
- package/dist/chunk-YEHIBPIX.js +137 -0
- package/dist/chunk-YEHIBPIX.js.map +1 -0
- package/dist/chunk-YKPPCV2Z.js +53 -0
- package/dist/chunk-YKPPCV2Z.js.map +1 -0
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
- package/dist/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/connect-XKQJOWR7.js +18 -0
- package/dist/connect-XKQJOWR7.js.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/RelatedListTab.d.ts.map +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts.map +1 -1
- package/dist/crud/record-chrome.d.ts.map +1 -1
- package/dist/entity-routes-PGU7EHAD.js +4 -0
- package/dist/{entity-routes-LXOBW5IB.js.map → entity-routes-PGU7EHAD.js.map} +1 -1
- package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
- package/dist/impersonation/ImpersonateDialog.d.ts.map +1 -1
- package/dist/impersonation/session.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11951 -33576
- package/dist/index.js.map +1 -1
- package/dist/lib/attention.d.ts +6 -0
- package/dist/lib/attention.d.ts.map +1 -1
- package/dist/lib/entity-routes.d.ts.map +1 -1
- package/dist/org/index.js +4 -2
- package/dist/plugins/PluginRegistryManager.d.ts.map +1 -1
- package/dist/settings/MessageTemplateRow.d.ts.map +1 -1
- package/dist/timeline/PersonTimeline.d.ts.map +1 -1
- package/dist/timeline/TimelineList.d.ts.map +1 -1
- package/dist/units/UnitRailPage.d.ts.map +1 -1
- package/dist/units/useUnitFilter.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/PersonTimeline-X7JTPUWJ.js +0 -3
- package/dist/ScheduleEditor-L2UJXKMY.js +0 -6
- package/dist/SubscriptionPage-IDVEFLZ5.js +0 -8
- package/dist/chunk-3YP7KW47.js.map +0 -1
- package/dist/chunk-E4MESMNR.js.map +0 -1
- package/dist/chunk-IXSY4GPK.js.map +0 -1
- package/dist/chunk-JOBRLBIQ.js.map +0 -1
- package/dist/chunk-KPYRTOCK.js.map +0 -1
- package/dist/chunk-OQARLKZN.js.map +0 -1
- package/dist/chunk-P5UIGQFY.js.map +0 -1
- package/dist/chunk-P6XUDT2P.js.map +0 -1
- package/dist/chunk-VX3WJLKK.js.map +0 -1
- package/dist/entity-routes-LXOBW5IB.js +0 -4
|
@@ -0,0 +1,1466 @@
|
|
|
1
|
+
import { checkAccess, guardLimit, useRightRailStore } from './chunk-PQ47MLTH.js';
|
|
2
|
+
import { invalidateLimit } from './chunk-S56FQKPL.js';
|
|
3
|
+
import { resolveEntityRoute, openEntity } from './chunk-ALCSIN3Z.js';
|
|
4
|
+
import { loadArea, useChatStore, CORE_QUERY_ENTITIES, resolveAttachmentDigest, listAttachmentNames, nonTabularAttachmentNames, readAllRows, ALWAYS_ON_TOOLS, lookupRecordRef, PROFILE_ACTIONS, resolveToolSearch } from './chunk-KLO7DKXX.js';
|
|
5
|
+
import { usePermissionsStore } from './chunk-UQ2NTFQC.js';
|
|
6
|
+
import { create } from 'zustand';
|
|
7
|
+
import { resolveDataProvider, emitDataChanged, deriveEntityKey, bulkCreate, matchesRange, getSupabaseClientOptional, getActiveTenantId, useTranslation, parseRangeFilter, isFilterObject } from '@fayz-ai/core';
|
|
8
|
+
import { profileHasSystemPermission } from '@fayz-ai/core/access';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import { WandSparkles } from 'lucide-react';
|
|
11
|
+
import { DropdownItem, Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalFooter } from '@fayz-ai/ui';
|
|
12
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
13
|
+
|
|
14
|
+
var ASK_AI_HANDOFF_ID = "ask-ai.handoff";
|
|
15
|
+
var KEY_DISMISSED = "fayz.assistant.invite.dismissed";
|
|
16
|
+
function session() {
|
|
17
|
+
try {
|
|
18
|
+
return typeof sessionStorage === "undefined" ? null : sessionStorage;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function dismissedIds() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(session()?.getItem(KEY_DISMISSED) ?? "[]");
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function rememberDismissal(id) {
|
|
31
|
+
const store = session();
|
|
32
|
+
if (!store) return;
|
|
33
|
+
store.setItem(KEY_DISMISSED, JSON.stringify([.../* @__PURE__ */ new Set([...dismissedIds(), id])]));
|
|
34
|
+
}
|
|
35
|
+
var useAssistantInviteStore = create((set, get) => ({
|
|
36
|
+
invite: null,
|
|
37
|
+
stage: "dock",
|
|
38
|
+
dockBox: null,
|
|
39
|
+
leaving: null,
|
|
40
|
+
setInvite: (invite, id) => {
|
|
41
|
+
if (!invite) {
|
|
42
|
+
if (id && get().invite?.id !== id) return;
|
|
43
|
+
const { invite: going, dockBox } = get();
|
|
44
|
+
set({
|
|
45
|
+
invite: null,
|
|
46
|
+
stage: "dock",
|
|
47
|
+
dockBox: null,
|
|
48
|
+
leaving: going && dockBox ? { invite: going, box: dockBox } : null
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (invite.role) loadArea(invite.role);
|
|
53
|
+
const hero = invite.auto !== false && !dismissedIds().includes(invite.id);
|
|
54
|
+
set({ invite, stage: hero ? "hero" : "dock", leaving: null });
|
|
55
|
+
},
|
|
56
|
+
setDockBox: (box) => {
|
|
57
|
+
const current = get().dockBox;
|
|
58
|
+
if (current?.left === box?.left && current?.width === box?.width) return;
|
|
59
|
+
set({ dockBox: box });
|
|
60
|
+
},
|
|
61
|
+
endLeave: () => set({ leaving: null }),
|
|
62
|
+
dismiss: () => {
|
|
63
|
+
const { invite } = get();
|
|
64
|
+
if (invite) rememberDismissal(invite.id);
|
|
65
|
+
set({ stage: "dock" });
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// src/lib/reveal-page.ts
|
|
70
|
+
function revealPage(delayMs = 1200) {
|
|
71
|
+
if (typeof window === "undefined" || window.matchMedia("(min-width: 768px)").matches) return;
|
|
72
|
+
const close = () => {
|
|
73
|
+
useChatStore.getState().setOpen(false);
|
|
74
|
+
if (useRightRailStore.getState().open) useRightRailStore.getState().close();
|
|
75
|
+
};
|
|
76
|
+
if (!useChatStore.getState().isStreaming) {
|
|
77
|
+
window.setTimeout(close, delayMs);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const stop = useChatStore.subscribe((state, prev) => {
|
|
81
|
+
if (prev.isStreaming && !state.isStreaming) {
|
|
82
|
+
stop();
|
|
83
|
+
window.setTimeout(close, delayMs);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/lib/agent-surface.ts
|
|
89
|
+
var entries = [];
|
|
90
|
+
var version = 0;
|
|
91
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
92
|
+
function emit() {
|
|
93
|
+
version++;
|
|
94
|
+
for (const l of listeners) l();
|
|
95
|
+
}
|
|
96
|
+
function registerSurface(entry) {
|
|
97
|
+
entries = [...entries.filter((e) => e.id !== entry.id), entry];
|
|
98
|
+
emit();
|
|
99
|
+
}
|
|
100
|
+
function unregisterSurface(id) {
|
|
101
|
+
const next = entries.filter((e) => e.id !== id);
|
|
102
|
+
if (next.length === entries.length) return;
|
|
103
|
+
entries = next;
|
|
104
|
+
emit();
|
|
105
|
+
}
|
|
106
|
+
function subscribeSurfaces(cb) {
|
|
107
|
+
listeners.add(cb);
|
|
108
|
+
return () => listeners.delete(cb);
|
|
109
|
+
}
|
|
110
|
+
function getSurfaceVersion() {
|
|
111
|
+
return version;
|
|
112
|
+
}
|
|
113
|
+
function getSurfaceTools() {
|
|
114
|
+
return [...entries].reverse().flatMap((e) => {
|
|
115
|
+
const c = e.get();
|
|
116
|
+
return c.tools?.map(({ tool }) => tool.role || !c.role ? tool : { ...tool, role: c.role }) ?? [];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function getActiveSurfaceContext() {
|
|
120
|
+
const snapshot = [];
|
|
121
|
+
const instructions = [];
|
|
122
|
+
for (const e of entries) {
|
|
123
|
+
const c = e.get();
|
|
124
|
+
if (c.describe) {
|
|
125
|
+
try {
|
|
126
|
+
snapshot.push(c.describe());
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (c.instructions) instructions.push(c.instructions);
|
|
131
|
+
}
|
|
132
|
+
return { snapshot, instructions };
|
|
133
|
+
}
|
|
134
|
+
function requestChatConfirmation(action) {
|
|
135
|
+
return useChatStore.getState().requestConfirmation(action);
|
|
136
|
+
}
|
|
137
|
+
var SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
138
|
+
"id",
|
|
139
|
+
"tenant_id",
|
|
140
|
+
"tenantId",
|
|
141
|
+
"created_at",
|
|
142
|
+
"createdAt",
|
|
143
|
+
"updated_at",
|
|
144
|
+
"updatedAt",
|
|
145
|
+
"deleted_at",
|
|
146
|
+
"deletedAt",
|
|
147
|
+
"created_by",
|
|
148
|
+
"createdBy"
|
|
149
|
+
]);
|
|
150
|
+
function normaliseOptions(raw) {
|
|
151
|
+
if (!Array.isArray(raw)) return void 0;
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const o of raw) {
|
|
154
|
+
if (typeof o === "string") out.push({ value: o });
|
|
155
|
+
else if (o && typeof o === "object" && typeof o.value === "string") {
|
|
156
|
+
const rec = o;
|
|
157
|
+
out.push({ value: rec.value, label: typeof rec.label === "string" ? rec.label : void 0 });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out.length ? out : void 0;
|
|
161
|
+
}
|
|
162
|
+
function cardFields(entity) {
|
|
163
|
+
return (entity.fields ?? []).filter((f) => f.showInForm !== false && !SYSTEM_FIELD_KEYS.has(String(f.key))).map((f) => {
|
|
164
|
+
const type = typeof f.type === "string" ? f.type : void 0;
|
|
165
|
+
return {
|
|
166
|
+
key: String(f.key),
|
|
167
|
+
label: typeof f.label === "string" ? f.label : void 0,
|
|
168
|
+
type,
|
|
169
|
+
required: f.required === true,
|
|
170
|
+
options: normaliseOptions(f.options),
|
|
171
|
+
placeholder: typeof f.placeholder === "string" ? f.placeholder : void 0,
|
|
172
|
+
hint: typeof f.hint === "string" ? f.hint : void 0,
|
|
173
|
+
min: typeof f.min === "number" ? f.min : void 0,
|
|
174
|
+
max: typeof f.max === "number" ? f.max : void 0,
|
|
175
|
+
default: f.defaultValue,
|
|
176
|
+
// A computed value has no input, and a relation needs its option list
|
|
177
|
+
// loaded from a table — neither is something to type into a chat card.
|
|
178
|
+
readOnly: type === "computed" || type === "relation" || !!f.relation
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
var handlers = /* @__PURE__ */ new Map();
|
|
183
|
+
function registerAIToolHandler(name, handler) {
|
|
184
|
+
handlers.set(name, handler);
|
|
185
|
+
}
|
|
186
|
+
function unregisterAIToolHandler(name) {
|
|
187
|
+
handlers.delete(name);
|
|
188
|
+
}
|
|
189
|
+
function hasAIToolHandler(name) {
|
|
190
|
+
return handlers.has(name);
|
|
191
|
+
}
|
|
192
|
+
var REGISTRY_PAGE_SIZE = 20;
|
|
193
|
+
var FILTERED_PAGE_SIZE = 200;
|
|
194
|
+
var FRESH = { cache: false };
|
|
195
|
+
function localDayBound(value, edge) {
|
|
196
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value.trim());
|
|
197
|
+
if (!dateOnly) return value;
|
|
198
|
+
const time = edge === "start" ? "00:00:00.000" : "23:59:59.999";
|
|
199
|
+
const parsed = /* @__PURE__ */ new Date(`${value.trim()}T${time}`);
|
|
200
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
|
|
201
|
+
}
|
|
202
|
+
function dateRangeFilter(args) {
|
|
203
|
+
const from = typeof args.from === "string" && args.from.trim() ? args.from.trim() : void 0;
|
|
204
|
+
const to = typeof args.to === "string" && args.to.trim() ? args.to.trim() : void 0;
|
|
205
|
+
if (!from && !to) return null;
|
|
206
|
+
const field = typeof args.dateField === "string" && args.dateField.trim() ? args.dateField.trim() : "createdAt";
|
|
207
|
+
const range = {};
|
|
208
|
+
if (from) range.gte = localDayBound(from, "start");
|
|
209
|
+
if (to) range.lte = localDayBound(to, "end");
|
|
210
|
+
return { field, range };
|
|
211
|
+
}
|
|
212
|
+
var MAX_TOOLS_PER_TURN = 36;
|
|
213
|
+
function errorResult(error, message) {
|
|
214
|
+
return { ok: false, content: JSON.stringify({ error, message }) };
|
|
215
|
+
}
|
|
216
|
+
function attachRefs(entity, rows) {
|
|
217
|
+
const archetype = entity.data?.archetype;
|
|
218
|
+
if (!archetype) return rows;
|
|
219
|
+
const kind = entity.data?.archetypeKind;
|
|
220
|
+
const entityKey = deriveEntityKey(entity);
|
|
221
|
+
return rows.map(
|
|
222
|
+
(r) => r.id ? {
|
|
223
|
+
...r,
|
|
224
|
+
ref: {
|
|
225
|
+
id: r.id,
|
|
226
|
+
resource: entity.data?.table,
|
|
227
|
+
archetype: kind ? `${archetype}:${kind}` : archetype,
|
|
228
|
+
entityKey
|
|
229
|
+
}
|
|
230
|
+
} : r
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
function rowContainsText(row, needle) {
|
|
234
|
+
const n = needle.toLowerCase();
|
|
235
|
+
return Object.values(row).some((v) => typeof v === "string" && v.toLowerCase().includes(n));
|
|
236
|
+
}
|
|
237
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
238
|
+
async function executeDataTool(target, args) {
|
|
239
|
+
const provider = resolveDataProvider(
|
|
240
|
+
target.entity,
|
|
241
|
+
target.mockData,
|
|
242
|
+
FRESH
|
|
243
|
+
);
|
|
244
|
+
const search = typeof args.search === "string" && args.search.trim() ? args.search.trim() : void 0;
|
|
245
|
+
if (search && UUID_RE.test(search)) {
|
|
246
|
+
try {
|
|
247
|
+
const byId = await provider.list({ filters: { id: search }, pageSize: 1 });
|
|
248
|
+
const row = byId.data[0];
|
|
249
|
+
if (row && String(row.id) === search) {
|
|
250
|
+
return {
|
|
251
|
+
entity: target.label,
|
|
252
|
+
search,
|
|
253
|
+
total: 1,
|
|
254
|
+
truncated: false,
|
|
255
|
+
records: attachRefs(target.entity, [row])
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const dateRange = dateRangeFilter(args);
|
|
262
|
+
const result = await provider.list({
|
|
263
|
+
search,
|
|
264
|
+
pageSize: dateRange ? FILTERED_PAGE_SIZE : REGISTRY_PAGE_SIZE,
|
|
265
|
+
...dateRange ? { filters: { [dateRange.field]: dateRange.range } } : {},
|
|
266
|
+
...typeof args.orderBy === "string" ? { sortBy: args.orderBy, sortDir: args.direction === "asc" ? "asc" : "desc" } : {}
|
|
267
|
+
});
|
|
268
|
+
let rows = result.data;
|
|
269
|
+
let total = result.total;
|
|
270
|
+
if (dateRange && rows.length) {
|
|
271
|
+
const key = resolveFieldKey(rows[0], dateRange.field);
|
|
272
|
+
if (!key) {
|
|
273
|
+
return {
|
|
274
|
+
error: "unknown_field",
|
|
275
|
+
message: `dateField "${dateRange.field}" does not exist on ${target.label}. Use one of the available fields and retry.`,
|
|
276
|
+
availableFields: Object.keys(rows[0])
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const narrowed = rows.filter((r) => matchesRange(r[key], dateRange.range));
|
|
280
|
+
if (narrowed.length !== rows.length) total = narrowed.length;
|
|
281
|
+
rows = narrowed;
|
|
282
|
+
}
|
|
283
|
+
const unmatched = !!search && rows.length > 0 && !rows.some((r) => rowContainsText(r, search));
|
|
284
|
+
return {
|
|
285
|
+
entity: target.label,
|
|
286
|
+
...search ? { search } : {},
|
|
287
|
+
...dateRange ? { dateRange: { field: dateRange.field, ...dateRange.range } } : {},
|
|
288
|
+
total,
|
|
289
|
+
// Truthful cap: the model is told the list was trimmed so it can say so
|
|
290
|
+
// rather than presenting one page as the whole set.
|
|
291
|
+
truncated: total > rows.length,
|
|
292
|
+
records: attachRefs(target.entity, rows),
|
|
293
|
+
...unmatched ? {
|
|
294
|
+
warning: "None of these rows contain the search text \u2014 they are RECENT records, not matches. Do NOT present them as the searched item; say it was not found or try another entity."
|
|
295
|
+
} : {}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function readOnlyRefusal(ctx, target, verb) {
|
|
299
|
+
const noun = target.entity.name.replace(/\s+/g, "").toLowerCase();
|
|
300
|
+
const plural = target.label.replace(/\s+/g, "").toLowerCase();
|
|
301
|
+
const wantedAction = verb === "created" ? "create" : "edit";
|
|
302
|
+
const candidates = (ctx.writeTools ?? []).filter(({ name, action }) => {
|
|
303
|
+
const n = name.toLowerCase();
|
|
304
|
+
if (n === "createrecord" || n === "updaterecord") return false;
|
|
305
|
+
if (!n.includes(noun) && !n.includes(plural)) return false;
|
|
306
|
+
if (action) return action === wantedAction;
|
|
307
|
+
return wantedAction === "create" ? /^(create|add|new)/.test(n) : /^(update|edit|set|change|move)/.test(n);
|
|
308
|
+
}).map((t) => t.name);
|
|
309
|
+
return {
|
|
310
|
+
error: "read_only",
|
|
311
|
+
message: `${target.label} is a read-only view \u2014 records cannot be ${verb} through this tool.` + (candidates.length ? ` Use ${candidates.join(" or ")} instead.` : ` This app has no tool that can change a ${target.entity.name.toLowerCase()}. Tell the user plainly that you cannot, and where in the app they can do it themselves.`) + " Do NOT substitute a create: making a new record instead of changing the existing one leaves the original in place and duplicates it.",
|
|
312
|
+
...candidates.length ? { useInstead: candidates } : {}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function resolveFieldKey(row, name) {
|
|
316
|
+
if (name in row) return name;
|
|
317
|
+
const camel = name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
318
|
+
if (camel in row) return camel;
|
|
319
|
+
const snake = name.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
|
|
320
|
+
if (snake in row) return snake;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
function resolveFilter(sample, label, name, value) {
|
|
324
|
+
const key = resolveFieldKey(sample, name);
|
|
325
|
+
if (!key) {
|
|
326
|
+
return {
|
|
327
|
+
error: "unknown_field",
|
|
328
|
+
message: `Filter field "${name}" does not exist on ${label}. Use one of the available fields and retry.`,
|
|
329
|
+
availableFields: Object.keys(sample)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const range = parseRangeFilter(value);
|
|
333
|
+
if (range) return { key, test: (row) => matchesRange(row[key], range) };
|
|
334
|
+
if (isFilterObject(value)) {
|
|
335
|
+
return {
|
|
336
|
+
error: "invalid_filter",
|
|
337
|
+
message: `Filter "${name}" is an object with no recognised bound. For a range use {"from":\u2026,"to":\u2026} (also accepted: gte/lte/gt/lt). For a date or period, prefer the from/to parameters of this tool instead of filters.`
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { key, test: (row) => String(row[key] ?? "") === String(value) };
|
|
341
|
+
}
|
|
342
|
+
async function executeAITool(name, args, context) {
|
|
343
|
+
const handler = handlers.get(name);
|
|
344
|
+
const dataTarget = context.dataTools.get(name);
|
|
345
|
+
if (!handler && !dataTarget) {
|
|
346
|
+
return errorResult(
|
|
347
|
+
"not_executable",
|
|
348
|
+
`The tool "${name}" is not available here. Do not retry it \u2014 tell the user what you cannot do, or use a different tool.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const result = handler ? await handler(args, context) : await executeDataTool(dataTarget, args);
|
|
353
|
+
return { ok: true, content: JSON.stringify(result ?? { ok: true }) };
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return errorResult("execution_failed", describeThrown(error));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function describeThrown(error) {
|
|
359
|
+
if (error instanceof Error) return error.message;
|
|
360
|
+
if (error && typeof error === "object") {
|
|
361
|
+
const e = error;
|
|
362
|
+
const parts = [
|
|
363
|
+
typeof e.message === "string" ? e.message : null,
|
|
364
|
+
typeof e.hint === "string" ? e.hint : null,
|
|
365
|
+
typeof e.code === "string" ? `(${e.code})` : null
|
|
366
|
+
].filter(Boolean);
|
|
367
|
+
if (parts.length) return parts.join(" ");
|
|
368
|
+
}
|
|
369
|
+
return typeof error === "string" && error ? error : "Unknown error";
|
|
370
|
+
}
|
|
371
|
+
function buildDataToolIndex(input) {
|
|
372
|
+
const index = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const [pluginId, defs] of input.registries) {
|
|
374
|
+
for (const registry of defs) {
|
|
375
|
+
if (registry.readOnly) continue;
|
|
376
|
+
const target = {
|
|
377
|
+
label: registry.entity.namePlural ?? registry.entity.name,
|
|
378
|
+
entity: registry.entity,
|
|
379
|
+
mockData: registry.mockData
|
|
380
|
+
};
|
|
381
|
+
index.set(input.registryToolName(pluginId, registry.id), target);
|
|
382
|
+
index.set(`key:${pluginId}:${registry.id}`, target);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const entity of input.entities) {
|
|
386
|
+
if (!entity.entityDef || entity.entityDef.agentHidden) continue;
|
|
387
|
+
const target = {
|
|
388
|
+
label: entity.labelPlural,
|
|
389
|
+
entity: entity.entityDef,
|
|
390
|
+
mockData: entity.mockData
|
|
391
|
+
};
|
|
392
|
+
index.set(input.entityToolName(entity.entityKey), target);
|
|
393
|
+
index.set(`key:${entity.entityKey}`, target);
|
|
394
|
+
}
|
|
395
|
+
for (const q of [...input.queryEntities ?? [], ...CORE_QUERY_ENTITIES]) {
|
|
396
|
+
index.set(`key:${q.key}`, {
|
|
397
|
+
label: q.entity.namePlural ?? q.entity.name,
|
|
398
|
+
entity: q.entity,
|
|
399
|
+
readOnly: !q.writable
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return index;
|
|
403
|
+
}
|
|
404
|
+
function openListWriteHint() {
|
|
405
|
+
const hasCreateHere = getSurfaceTools().some((t) => t.name === "createRecordHere");
|
|
406
|
+
return hasCreateHere ? " The list the user has open CAN create this one: call `createRecordHere` with `values`, using the field keys in the screen state." : "";
|
|
407
|
+
}
|
|
408
|
+
function resolveKeyedTarget(ctx, args) {
|
|
409
|
+
const key = typeof args.entity === "string" ? args.entity : "";
|
|
410
|
+
const target = ctx.dataTools.get(`key:${key}`);
|
|
411
|
+
if (!target) {
|
|
412
|
+
return {
|
|
413
|
+
error: {
|
|
414
|
+
ok: false,
|
|
415
|
+
error: "unknown_entity",
|
|
416
|
+
// Said as plainly as it can be said: the failure above this one was a
|
|
417
|
+
// reply that described a record nobody had written.
|
|
418
|
+
message: `NOTHING WAS WRITTEN. No entity "${key}" is available to these tools \u2014 do not tell the user it worked. Use one of the keys listed in the tool description.${openListWriteHint()}`
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const permission = target.entity.permission;
|
|
423
|
+
if (permission) {
|
|
424
|
+
const access = checkAccess(permission.feature, permission.action);
|
|
425
|
+
if (!access.allowed) return { error: access };
|
|
426
|
+
}
|
|
427
|
+
return { target };
|
|
428
|
+
}
|
|
429
|
+
function sameField(a, b) {
|
|
430
|
+
const norm = (s) => s.replace(/[_-]/g, "").toLowerCase();
|
|
431
|
+
return norm(a) === norm(b);
|
|
432
|
+
}
|
|
433
|
+
function matchDeclaredFields(target, values) {
|
|
434
|
+
const declaredKeys = (target.entity.fields ?? []).map((f) => f.key);
|
|
435
|
+
const clean = {};
|
|
436
|
+
const unknown = [];
|
|
437
|
+
for (const [k, v] of Object.entries(values)) {
|
|
438
|
+
const match = declaredKeys.find((d) => sameField(d, k)) ?? null;
|
|
439
|
+
if (match) clean[match] = v;
|
|
440
|
+
else unknown.push(k);
|
|
441
|
+
}
|
|
442
|
+
if (!unknown.length) return { clean };
|
|
443
|
+
return {
|
|
444
|
+
error: {
|
|
445
|
+
error: "unknown_field",
|
|
446
|
+
message: `Unknown field(s) ${unknown.join(", ")} for ${target.label}. Use only the available fields and retry.`,
|
|
447
|
+
availableFields: declaredKeys
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async function resolveAttachedRecords(ctx, matches) {
|
|
452
|
+
const ownerIds = /* @__PURE__ */ new Set();
|
|
453
|
+
for (const m of matches) {
|
|
454
|
+
const target = ctx.dataTools.get(`key:${m.key}`);
|
|
455
|
+
if (target?.entity.data?.archetype !== "person") continue;
|
|
456
|
+
for (const r of m.records) {
|
|
457
|
+
const id = r.id;
|
|
458
|
+
if (typeof id === "string") ownerIds.add(id);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!ownerIds.size) return null;
|
|
462
|
+
const out = {};
|
|
463
|
+
for (const q of CORE_QUERY_ENTITIES) {
|
|
464
|
+
const target = ctx.dataTools.get(`key:${q.key}`);
|
|
465
|
+
if (!target) continue;
|
|
466
|
+
const permission = target.entity.permission;
|
|
467
|
+
if (permission && !checkAccess(permission.feature, permission.action).allowed) continue;
|
|
468
|
+
try {
|
|
469
|
+
const provider = resolveDataProvider(target.entity, target.mockData);
|
|
470
|
+
const result = await provider.list({ pageSize: 50 });
|
|
471
|
+
const rows = result.data.filter((row) => {
|
|
472
|
+
const owner = row.ownerId ?? row.owner_id;
|
|
473
|
+
return typeof owner === "string" && ownerIds.has(owner);
|
|
474
|
+
});
|
|
475
|
+
if (rows.length) out[q.key] = rows;
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return Object.keys(out).length ? out : null;
|
|
480
|
+
}
|
|
481
|
+
registerAIToolHandler("findAnything", async (args, ctx) => {
|
|
482
|
+
const search = typeof args.search === "string" ? args.search.trim() : "";
|
|
483
|
+
if (!search) return { error: "missing_search", message: "Provide the name or text to look up." };
|
|
484
|
+
const seen = /* @__PURE__ */ new Set();
|
|
485
|
+
const candidates = [];
|
|
486
|
+
for (const [key, target] of ctx.dataTools) {
|
|
487
|
+
if (!key.startsWith("key:") || seen.has(target)) continue;
|
|
488
|
+
seen.add(target);
|
|
489
|
+
candidates.push({ key: key.slice(4), target });
|
|
490
|
+
}
|
|
491
|
+
let skippedForPermission = 0;
|
|
492
|
+
const allowed = candidates.filter(({ target }) => {
|
|
493
|
+
const permission = target.entity.permission;
|
|
494
|
+
if (!permission) return true;
|
|
495
|
+
if (checkAccess(permission.feature, permission.action).allowed) return true;
|
|
496
|
+
skippedForPermission += 1;
|
|
497
|
+
return false;
|
|
498
|
+
});
|
|
499
|
+
allowed.sort((a, b) => {
|
|
500
|
+
const ap = a.target.entity.data?.archetype === "person" ? 0 : 1;
|
|
501
|
+
const bp = b.target.entity.data?.archetype === "person" ? 0 : 1;
|
|
502
|
+
return ap - bp || a.key.localeCompare(b.key);
|
|
503
|
+
});
|
|
504
|
+
const MAX_TARGETS = 20;
|
|
505
|
+
const searched = allowed.slice(0, MAX_TARGETS);
|
|
506
|
+
const settled = await Promise.allSettled(
|
|
507
|
+
searched.map(async ({ key, target }) => {
|
|
508
|
+
const provider = resolveDataProvider(
|
|
509
|
+
target.entity,
|
|
510
|
+
target.mockData
|
|
511
|
+
);
|
|
512
|
+
const result = await provider.list({ search, pageSize: 5 });
|
|
513
|
+
return {
|
|
514
|
+
key,
|
|
515
|
+
label: target.label,
|
|
516
|
+
total: result.total,
|
|
517
|
+
records: attachRefs(target.entity, result.data)
|
|
518
|
+
};
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
const matches = settled.filter((r) => r.status === "fulfilled" && r.value.records.length > 0).map((r) => r.value);
|
|
522
|
+
const attached = await resolveAttachedRecords(ctx, matches);
|
|
523
|
+
return {
|
|
524
|
+
query: search,
|
|
525
|
+
matches,
|
|
526
|
+
...attached ? { attached } : {},
|
|
527
|
+
searchedEntities: searched.length,
|
|
528
|
+
...allowed.length > MAX_TARGETS ? { targetsTruncated: true } : {},
|
|
529
|
+
...skippedForPermission ? { skippedForPermission } : {},
|
|
530
|
+
...matches.length === 0 ? { hint: "No records matched anywhere the user can see. Say so \u2014 do not retry other search tools with the same text." } : {}
|
|
531
|
+
};
|
|
532
|
+
});
|
|
533
|
+
registerAIToolHandler("createRecord", async (args, ctx) => {
|
|
534
|
+
const resolved = resolveKeyedTarget(ctx, args);
|
|
535
|
+
if ("error" in resolved) return resolved.error;
|
|
536
|
+
const { target } = resolved;
|
|
537
|
+
if (target.readOnly) return readOnlyRefusal(ctx, target, "created");
|
|
538
|
+
const permission = target.entity.permission;
|
|
539
|
+
if (permission) {
|
|
540
|
+
const access = checkAccess(permission.feature, "create");
|
|
541
|
+
if (!access.allowed) return access;
|
|
542
|
+
}
|
|
543
|
+
if (target.entity.limitKey) {
|
|
544
|
+
const limit = await guardLimit(target.entity.limitKey);
|
|
545
|
+
if (!limit.allowed) return limit;
|
|
546
|
+
}
|
|
547
|
+
const values = args.values ?? {};
|
|
548
|
+
const declared = target.entity.fields ?? [];
|
|
549
|
+
const matched = matchDeclaredFields(target, values);
|
|
550
|
+
if ("error" in matched) return matched.error;
|
|
551
|
+
const { clean } = matched;
|
|
552
|
+
const isEmpty = (v) => v === void 0 || v === null || v === "";
|
|
553
|
+
const missingRequired = declared.filter((f) => f.required && isEmpty(clean[f.key]));
|
|
554
|
+
const mustResolveFirst = missingRequired.filter(
|
|
555
|
+
(f) => f.type === "computed" || f.type === "relation" || !!f.relation || f.showInForm === false
|
|
556
|
+
);
|
|
557
|
+
if (mustResolveFirst.length) {
|
|
558
|
+
return {
|
|
559
|
+
error: "missing_required",
|
|
560
|
+
message: `Missing required field(s) that must be resolved to a record first: ${mustResolveFirst.map((f) => f.key).join(", ")}. Look them up (search for the referenced record), then retry.`,
|
|
561
|
+
availableFields: declared.map((f) => f.key)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const dupName = typeof clean.name === "string" ? clean.name.trim() : "";
|
|
565
|
+
if (dupName && args.allowDuplicate !== true) {
|
|
566
|
+
const provider0 = resolveDataProvider(
|
|
567
|
+
target.entity,
|
|
568
|
+
target.mockData
|
|
569
|
+
);
|
|
570
|
+
const existing = await provider0.list({ search: dupName, pageSize: 3 });
|
|
571
|
+
const dupes = existing.data.filter(
|
|
572
|
+
(r) => String(r.name ?? "").trim().toLowerCase() === dupName.toLowerCase()
|
|
573
|
+
);
|
|
574
|
+
if (dupes.length) {
|
|
575
|
+
return {
|
|
576
|
+
error: "possible_duplicate",
|
|
577
|
+
message: `A ${target.entity.name} named "${dupName}" already exists. If the user wants to CHANGE that existing record, use updateRecord with its id; only retry createRecord with allowDuplicate:true if they explicitly want a second one.`,
|
|
578
|
+
matches: dupes
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const decision = await requestChatConfirmation({
|
|
583
|
+
id: `create-${Date.now()}`,
|
|
584
|
+
toolName: "createRecord",
|
|
585
|
+
title: `Criar ${target.entity.name}`,
|
|
586
|
+
params: clean,
|
|
587
|
+
plane: "client",
|
|
588
|
+
// Scoped grant: "always create clients", never "always create anything".
|
|
589
|
+
scope: typeof args.entity === "string" ? args.entity : void 0,
|
|
590
|
+
scopeLabel: `Criar ${target.entity.name}`,
|
|
591
|
+
fields: cardFields(target.entity)
|
|
592
|
+
});
|
|
593
|
+
const approved = decision.approved;
|
|
594
|
+
if (decision.values) {
|
|
595
|
+
for (const key of Object.keys(clean)) delete clean[key];
|
|
596
|
+
Object.assign(clean, decision.values);
|
|
597
|
+
}
|
|
598
|
+
if (!approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
599
|
+
const stillMissing = declared.filter((f) => f.required && isEmpty(clean[f.key])).map((f) => f.key);
|
|
600
|
+
if (stillMissing.length) {
|
|
601
|
+
return {
|
|
602
|
+
error: "missing_required",
|
|
603
|
+
message: `Still missing required field(s): ${stillMissing.join(", ")}. Ask the user for them, then retry.`,
|
|
604
|
+
availableFields: declared.map((f) => f.key)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const provider = resolveDataProvider(
|
|
608
|
+
target.entity,
|
|
609
|
+
target.mockData
|
|
610
|
+
);
|
|
611
|
+
const record = await provider.create(clean);
|
|
612
|
+
if (target.entity.limitKey) invalidateLimit(target.entity.limitKey);
|
|
613
|
+
emitDataChanged({
|
|
614
|
+
table: target.entity.data?.table,
|
|
615
|
+
entityKey: typeof args.entity === "string" ? args.entity : void 0,
|
|
616
|
+
archetype: target.entity.data?.archetype,
|
|
617
|
+
op: "create",
|
|
618
|
+
id: record.id,
|
|
619
|
+
source: "agent"
|
|
620
|
+
});
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
record,
|
|
624
|
+
ref: {
|
|
625
|
+
id: record.id,
|
|
626
|
+
label: String(record.name ?? target.entity.name),
|
|
627
|
+
resource: target.entity.data?.table,
|
|
628
|
+
archetype: target.entity.data?.archetype ? `${target.entity.data.archetype}${target.entity.data.archetypeKind ? ":" + target.entity.data.archetypeKind : ""}` : void 0,
|
|
629
|
+
entityKey: deriveEntityKey(target.entity)
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
registerAIToolHandler("updateRecord", async (args, ctx) => {
|
|
634
|
+
const resolved = resolveKeyedTarget(ctx, args);
|
|
635
|
+
if ("error" in resolved) return resolved.error;
|
|
636
|
+
const { target } = resolved;
|
|
637
|
+
if (target.readOnly) return readOnlyRefusal(ctx, target, "updated");
|
|
638
|
+
const id = typeof args.id === "string" ? args.id : "";
|
|
639
|
+
if (!id) return { error: "missing_id", message: "Provide the id of the record to update (from this conversation or a search)." };
|
|
640
|
+
const permission = target.entity.permission;
|
|
641
|
+
if (permission) {
|
|
642
|
+
const access = checkAccess(permission.feature, "edit");
|
|
643
|
+
if (!access.allowed) return access;
|
|
644
|
+
}
|
|
645
|
+
const values = args.values ?? {};
|
|
646
|
+
const matched = matchDeclaredFields(target, values);
|
|
647
|
+
if ("error" in matched) return matched.error;
|
|
648
|
+
const { clean } = matched;
|
|
649
|
+
if (!Object.keys(clean).length) {
|
|
650
|
+
return { error: "empty_update", message: "No valid fields to change were provided." };
|
|
651
|
+
}
|
|
652
|
+
const decision = await requestChatConfirmation({
|
|
653
|
+
id: `update-${Date.now()}`,
|
|
654
|
+
toolName: "updateRecord",
|
|
655
|
+
title: `Atualizar ${target.entity.name}`,
|
|
656
|
+
params: { id, ...clean },
|
|
657
|
+
plane: "client",
|
|
658
|
+
scope: typeof args.entity === "string" ? args.entity : void 0,
|
|
659
|
+
scopeLabel: `Atualizar ${target.entity.name}`,
|
|
660
|
+
fields: cardFields(target.entity),
|
|
661
|
+
partial: true
|
|
662
|
+
});
|
|
663
|
+
if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
664
|
+
if (decision.values) {
|
|
665
|
+
for (const key of Object.keys(clean)) delete clean[key];
|
|
666
|
+
for (const [k, v] of Object.entries(decision.values)) if (k !== "id") clean[k] = v;
|
|
667
|
+
}
|
|
668
|
+
const provider = resolveDataProvider(
|
|
669
|
+
target.entity,
|
|
670
|
+
target.mockData
|
|
671
|
+
);
|
|
672
|
+
const record = await provider.update(id, clean);
|
|
673
|
+
emitDataChanged({
|
|
674
|
+
table: target.entity.data?.table,
|
|
675
|
+
entityKey: typeof args.entity === "string" ? args.entity : void 0,
|
|
676
|
+
archetype: target.entity.data?.archetype,
|
|
677
|
+
op: "update",
|
|
678
|
+
id,
|
|
679
|
+
source: "agent"
|
|
680
|
+
});
|
|
681
|
+
return {
|
|
682
|
+
ok: true,
|
|
683
|
+
record,
|
|
684
|
+
ref: {
|
|
685
|
+
id,
|
|
686
|
+
label: String(record.name ?? target.entity.name),
|
|
687
|
+
resource: target.entity.data?.table,
|
|
688
|
+
archetype: target.entity.data?.archetype ? `${target.entity.data.archetype}${target.entity.data.archetypeKind ? ":" + target.entity.data.archetypeKind : ""}` : void 0,
|
|
689
|
+
entityKey: deriveEntityKey(target.entity)
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
});
|
|
693
|
+
registerAIToolHandler("searchRecords", async (args, ctx) => {
|
|
694
|
+
const resolved = resolveKeyedTarget(ctx, args);
|
|
695
|
+
if ("error" in resolved) return resolved.error;
|
|
696
|
+
const base = await executeDataTool(resolved.target, args);
|
|
697
|
+
if ("error" in base) return base;
|
|
698
|
+
const filters = args.filters ?? {};
|
|
699
|
+
const keys = Object.keys(filters);
|
|
700
|
+
if (!keys.length) return base;
|
|
701
|
+
let rows = base.records;
|
|
702
|
+
for (const k of keys) {
|
|
703
|
+
const sample = rows[0] ?? base.records[0];
|
|
704
|
+
if (!sample) break;
|
|
705
|
+
const f = resolveFilter(sample, resolved.target.label, k, filters[k]);
|
|
706
|
+
if ("error" in f) return f;
|
|
707
|
+
rows = rows.filter(f.test);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
...base,
|
|
711
|
+
records: rows,
|
|
712
|
+
filtered: keys,
|
|
713
|
+
total: rows.length,
|
|
714
|
+
// A filter that empties a non-empty page is ambiguous — "no record matches"
|
|
715
|
+
// and "the filter was wrong" look identical to the model, and it picks the
|
|
716
|
+
// first. Name the difference so it can retry instead of asserting absence.
|
|
717
|
+
...rows.length === 0 && base.records.length > 0 ? {
|
|
718
|
+
warning: `${base.records.length} record(s) were read but the filters ${JSON.stringify(filters)} excluded all of them. Check the filter values against the fields on those records before telling the user nothing exists.`
|
|
719
|
+
} : {}
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
registerAIToolHandler("proposeImport", async (args, ctx) => {
|
|
723
|
+
const resolved = resolveKeyedTarget(ctx, args);
|
|
724
|
+
if ("error" in resolved) return resolved.error;
|
|
725
|
+
const { target } = resolved;
|
|
726
|
+
const fileName = typeof args.file === "string" ? args.file : void 0;
|
|
727
|
+
const digest = resolveAttachmentDigest(fileName);
|
|
728
|
+
if (!digest) {
|
|
729
|
+
const names = listAttachmentNames();
|
|
730
|
+
const other = nonTabularAttachmentNames();
|
|
731
|
+
return {
|
|
732
|
+
error: "no_file",
|
|
733
|
+
message: names.length ? `No readable file called "${fileName ?? ""}". Attached right now: ${names.join(", ")}.` : other.length ? `${other.join(", ")} is attached, but it has no columns \u2014 importing is for spreadsheets and CSVs. If it is a photo or a scan, READ IT: it is in front of you, so answer from what is written on it and use the tool that matches what the user asked for. Do not ask for a CSV.` : "There is no file attached to this conversation. Ask the user to attach a CSV or a spreadsheet."
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const headers = (digest.headers ?? []).filter((h) => h.trim());
|
|
737
|
+
const fields = (target.entity.fields ?? []).filter(
|
|
738
|
+
(f) => f.showInForm !== false && !SYSTEM_FIELD_KEYS.has(String(f.key))
|
|
739
|
+
);
|
|
740
|
+
const fieldKeys = new Set(fields.map((f) => String(f.key)));
|
|
741
|
+
const proposed = args.mapping ?? {};
|
|
742
|
+
const accepted = {};
|
|
743
|
+
const rejected = [];
|
|
744
|
+
for (const [column, field] of Object.entries(proposed)) {
|
|
745
|
+
if (!headers.includes(column)) {
|
|
746
|
+
rejected.push(`column "${column}" is not in the file`);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (typeof field !== "string" || !fieldKeys.has(field)) {
|
|
750
|
+
rejected.push(`"${column}" \u2192 "${String(field)}" is not a field of ${target.label}`);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
accepted[column] = field;
|
|
754
|
+
}
|
|
755
|
+
const options = [
|
|
756
|
+
...fields.map((f) => ({ value: String(f.key), label: f.label ?? String(f.key) }))
|
|
757
|
+
];
|
|
758
|
+
const decision = await requestChatConfirmation({
|
|
759
|
+
id: `import-${digest.name}`,
|
|
760
|
+
toolName: "proposeImport",
|
|
761
|
+
title: `${digest.name} \u2192 ${target.label}`,
|
|
762
|
+
params: accepted,
|
|
763
|
+
plane: "client",
|
|
764
|
+
scope: typeof args.entity === "string" ? args.entity : void 0,
|
|
765
|
+
scopeLabel: `Importar para ${target.label}`,
|
|
766
|
+
// One row per column of the FILE, so a column the model skipped is visibly
|
|
767
|
+
// unmapped instead of silently absent.
|
|
768
|
+
fields: headers.map((header) => ({
|
|
769
|
+
key: header,
|
|
770
|
+
label: header,
|
|
771
|
+
options,
|
|
772
|
+
hint: digest.columnTypes?.find((c) => c.name === header)?.note
|
|
773
|
+
})),
|
|
774
|
+
partial: true
|
|
775
|
+
});
|
|
776
|
+
if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
777
|
+
const mapping = decision.values ?? accepted;
|
|
778
|
+
const mapped = Object.keys(mapping).length;
|
|
779
|
+
return {
|
|
780
|
+
ok: true,
|
|
781
|
+
file: digest.name,
|
|
782
|
+
entity: target.label,
|
|
783
|
+
rows: digest.rowCount,
|
|
784
|
+
mapping,
|
|
785
|
+
unmapped: headers.filter((h) => !mapping[h]),
|
|
786
|
+
...rejected.length ? { ignoredFromYourProposal: rejected } : {},
|
|
787
|
+
nextStep: "commitImport",
|
|
788
|
+
message: `The user confirmed the mapping: ${mapped} of ${headers.length} columns are mapped for ${digest.rowCount ?? "?"} rows. NOW CALL commitImport with this exact mapping to write the rows \u2014 that is the next and final step, and the user is waiting for it. Do not announce it, do not ask again, and do not repeat the mapping back line by line.`
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
registerAIToolHandler("commitImport", async (args, ctx) => {
|
|
792
|
+
const resolved = resolveKeyedTarget(ctx, args);
|
|
793
|
+
if ("error" in resolved) return resolved.error;
|
|
794
|
+
const { target } = resolved;
|
|
795
|
+
const digest = resolveAttachmentDigest(typeof args.file === "string" ? args.file : void 0);
|
|
796
|
+
if (!digest) {
|
|
797
|
+
return {
|
|
798
|
+
error: "no_file",
|
|
799
|
+
message: "The file is no longer in this conversation. Ask the user to attach it again."
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const mapping = args.mapping ?? {};
|
|
803
|
+
const columns = Object.keys(mapping).filter((c) => typeof mapping[c] === "string" && mapping[c]);
|
|
804
|
+
if (!columns.length) {
|
|
805
|
+
return {
|
|
806
|
+
error: "empty_mapping",
|
|
807
|
+
message: "No column is mapped. Run proposeImport first and pass the mapping it returns."
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const permission = target.entity.permission;
|
|
811
|
+
if (permission) {
|
|
812
|
+
const access = checkAccess(permission.feature, "create");
|
|
813
|
+
if (!access.allowed) return access;
|
|
814
|
+
}
|
|
815
|
+
let rows;
|
|
816
|
+
try {
|
|
817
|
+
rows = await readAllRows(digest);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
return { error: "unreadable", message: error instanceof Error ? error.message : "Could not read the file." };
|
|
820
|
+
}
|
|
821
|
+
const records = rows.map((row) => {
|
|
822
|
+
const record = {};
|
|
823
|
+
for (const column of columns) {
|
|
824
|
+
const value = row[column];
|
|
825
|
+
if (value !== void 0 && value !== "") record[mapping[column]] = value;
|
|
826
|
+
}
|
|
827
|
+
return record;
|
|
828
|
+
}).filter((r) => Object.keys(r).length);
|
|
829
|
+
if (!records.length) {
|
|
830
|
+
return { error: "no_rows", message: "Every row is empty under this mapping. Check the column names." };
|
|
831
|
+
}
|
|
832
|
+
if (target.entity.limitKey) {
|
|
833
|
+
const limit = await guardLimit(target.entity.limitKey, records.length);
|
|
834
|
+
if (!limit.allowed) return limit;
|
|
835
|
+
}
|
|
836
|
+
const decision = await requestChatConfirmation({
|
|
837
|
+
id: `commit-${digest.name}`,
|
|
838
|
+
toolName: "commitImport",
|
|
839
|
+
title: `Importar ${records.length} \xD7 ${target.entity.name}`,
|
|
840
|
+
params: {
|
|
841
|
+
arquivo: digest.name,
|
|
842
|
+
registros: records.length,
|
|
843
|
+
campos: columns.map((c) => `${c} \u2192 ${mapping[c]}`).join(", ")
|
|
844
|
+
},
|
|
845
|
+
plane: "client",
|
|
846
|
+
scope: typeof args.entity === "string" ? args.entity : void 0,
|
|
847
|
+
scopeLabel: `Importar ${target.label}`
|
|
848
|
+
});
|
|
849
|
+
if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
850
|
+
const batchId = `imp-${Date.now().toString(36)}`;
|
|
851
|
+
const canStamp = (target.entity.fields ?? []).some((f) => String(f.key) === "importBatchId");
|
|
852
|
+
const provider = resolveDataProvider(target.entity, target.mockData, FRESH);
|
|
853
|
+
const result = await bulkCreate(
|
|
854
|
+
provider,
|
|
855
|
+
canStamp ? records.map((r) => ({ ...r, importBatchId: batchId })) : records,
|
|
856
|
+
{ chunkSize: 200 }
|
|
857
|
+
);
|
|
858
|
+
emitDataChanged({
|
|
859
|
+
table: target.entity.data?.table,
|
|
860
|
+
entityKey: typeof args.entity === "string" ? args.entity : void 0,
|
|
861
|
+
archetype: target.entity.data?.archetype,
|
|
862
|
+
op: "create",
|
|
863
|
+
source: "agent"
|
|
864
|
+
});
|
|
865
|
+
if (target.entity.limitKey) invalidateLimit(target.entity.limitKey);
|
|
866
|
+
return {
|
|
867
|
+
ok: result.created > 0,
|
|
868
|
+
...canStamp ? { batchId } : {},
|
|
869
|
+
created: result.created,
|
|
870
|
+
failed: result.failed.length,
|
|
871
|
+
// Row numbers, not row contents — the model never needs the data, and a
|
|
872
|
+
// failure list of 200 rows would swamp the turn.
|
|
873
|
+
firstErrors: result.failed.slice(0, 5).map((f) => ({ row: f.index + 2, reason: f.message })),
|
|
874
|
+
message: `${result.created} of ${records.length} rows were created` + (result.failed.length ? `. ${result.failed.length} failed \u2014 tell the user which rows and why, and offer to fix and retry those.` : ". Tell the user it is done.")
|
|
875
|
+
};
|
|
876
|
+
});
|
|
877
|
+
registerAIToolHandler("queryData", async (args, ctx) => {
|
|
878
|
+
const resolved = resolveKeyedTarget(ctx, args);
|
|
879
|
+
if ("error" in resolved) return resolved.error;
|
|
880
|
+
const { target } = resolved;
|
|
881
|
+
const metric = args.metric === "sum" || args.metric === "avg" ? args.metric : "count";
|
|
882
|
+
let field = typeof args.field === "string" ? args.field : void 0;
|
|
883
|
+
if (metric !== "count" && !field) {
|
|
884
|
+
return { error: "missing_field", message: `metric "${metric}" needs a "field" to aggregate.` };
|
|
885
|
+
}
|
|
886
|
+
const provider = resolveDataProvider(
|
|
887
|
+
target.entity,
|
|
888
|
+
target.mockData,
|
|
889
|
+
FRESH
|
|
890
|
+
);
|
|
891
|
+
const dateRange = dateRangeFilter(args);
|
|
892
|
+
const page = await provider.list({
|
|
893
|
+
pageSize: 1e3,
|
|
894
|
+
...dateRange ? { filters: { [dateRange.field]: dateRange.range } } : {}
|
|
895
|
+
});
|
|
896
|
+
let rows = page.data;
|
|
897
|
+
const sampleRow = rows[0];
|
|
898
|
+
const availableFields = sampleRow ? Object.keys(sampleRow) : [];
|
|
899
|
+
const requireField = (name) => {
|
|
900
|
+
if (!sampleRow) return name;
|
|
901
|
+
const resolved2 = resolveFieldKey(sampleRow, name);
|
|
902
|
+
if (resolved2) return resolved2;
|
|
903
|
+
return {
|
|
904
|
+
error: "unknown_field",
|
|
905
|
+
message: `Field "${name}" does not exist on ${target.label}. Use one of the available fields and retry.`,
|
|
906
|
+
availableFields
|
|
907
|
+
};
|
|
908
|
+
};
|
|
909
|
+
if (dateRange) {
|
|
910
|
+
const resolved2 = requireField(dateRange.field);
|
|
911
|
+
if (typeof resolved2 !== "string") return resolved2;
|
|
912
|
+
rows = rows.filter((r) => matchesRange(r[resolved2], dateRange.range));
|
|
913
|
+
}
|
|
914
|
+
const filters = args.filters ?? {};
|
|
915
|
+
for (const [k, v] of Object.entries(filters)) {
|
|
916
|
+
if (!sampleRow) break;
|
|
917
|
+
const f = resolveFilter(sampleRow, target.label, k, v);
|
|
918
|
+
if ("error" in f) return f;
|
|
919
|
+
rows = rows.filter(f.test);
|
|
920
|
+
}
|
|
921
|
+
const aggregate = (subset) => {
|
|
922
|
+
if (metric === "count") return subset.length;
|
|
923
|
+
const values = subset.map((r) => Number(r[field])).filter((n) => !Number.isNaN(n));
|
|
924
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
925
|
+
if (metric === "sum") return Math.round(sum * 100) / 100;
|
|
926
|
+
return values.length ? Math.round(sum / values.length * 100) / 100 : 0;
|
|
927
|
+
};
|
|
928
|
+
if (field && rows[0]) {
|
|
929
|
+
const resolved2 = resolveFieldKey(rows[0], field);
|
|
930
|
+
if (!resolved2) {
|
|
931
|
+
return {
|
|
932
|
+
error: "unknown_field",
|
|
933
|
+
message: `Field "${field}" does not exist on ${target.label}. Use one of the available fields and retry.`,
|
|
934
|
+
availableFields
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
field = resolved2;
|
|
938
|
+
}
|
|
939
|
+
let groupBy = typeof args.groupBy === "string" ? args.groupBy : void 0;
|
|
940
|
+
if (groupBy && rows[0]) {
|
|
941
|
+
const resolved2 = resolveFieldKey(rows[0], groupBy);
|
|
942
|
+
if (!resolved2) {
|
|
943
|
+
return {
|
|
944
|
+
error: "unknown_field",
|
|
945
|
+
message: `Field "${groupBy}" does not exist on ${target.label}. Use one of the available fields and retry.`,
|
|
946
|
+
availableFields
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
groupBy = resolved2;
|
|
950
|
+
}
|
|
951
|
+
const sampled = page.total > page.data.length;
|
|
952
|
+
if (groupBy) {
|
|
953
|
+
const groups = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const r of rows) {
|
|
955
|
+
const g = String(r[groupBy] ?? "\u2014");
|
|
956
|
+
if (!groups.has(g)) groups.set(g, []);
|
|
957
|
+
groups.get(g).push(r);
|
|
958
|
+
}
|
|
959
|
+
const labelKey = groupBy.endsWith("Id") ? resolveFieldKey(rows[0] ?? {}, groupBy.slice(0, -2) + "Name") : null;
|
|
960
|
+
return {
|
|
961
|
+
entity: target.label,
|
|
962
|
+
metric,
|
|
963
|
+
...field ? { field } : {},
|
|
964
|
+
groupBy,
|
|
965
|
+
groups: Array.from(groups.entries()).map(([group, subset]) => ({
|
|
966
|
+
group,
|
|
967
|
+
...labelKey && subset[0] ? { label: subset[0][labelKey] } : {},
|
|
968
|
+
value: aggregate(subset),
|
|
969
|
+
rows: subset.length
|
|
970
|
+
})).sort((a, b) => Number(b.value) - Number(a.value)),
|
|
971
|
+
matchedRows: rows.length,
|
|
972
|
+
sampled
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
entity: target.label,
|
|
977
|
+
metric,
|
|
978
|
+
...field ? { field } : {},
|
|
979
|
+
value: aggregate(rows),
|
|
980
|
+
matchedRows: rows.length,
|
|
981
|
+
sampled
|
|
982
|
+
};
|
|
983
|
+
});
|
|
984
|
+
function buildAgentToolset(tools, { dataTools, activePluginId, pinned, loaded, maxTools = MAX_TOOLS_PER_TURN }) {
|
|
985
|
+
const executable = tools.filter(
|
|
986
|
+
(t) => handlers.has(t.name) || dataTools.has(t.name) || t.execution?.plane === "server" && t.execution.kind === "rpc"
|
|
987
|
+
);
|
|
988
|
+
const ofPlugin = (t) => !!activePluginId && t.id.startsWith(`${activePluginId}.`);
|
|
989
|
+
const CORE_PINNED = /* @__PURE__ */ new Set([...ALWAYS_ON_TOOLS, "findTools"]);
|
|
990
|
+
const pinnedBand = executable.filter((t) => pinned?.has(t.name)).sort((a, b) => (CORE_PINNED.has(a.name) ? 0 : 1) - (CORE_PINNED.has(b.name) ? 0 : 1));
|
|
991
|
+
const ranked = pinned ? [
|
|
992
|
+
...pinnedBand,
|
|
993
|
+
...executable.filter((t) => !pinned.has(t.name) && loaded?.has(t.name)),
|
|
994
|
+
...executable.filter((t) => !pinned.has(t.name) && !loaded?.has(t.name) && ofPlugin(t))
|
|
995
|
+
] : activePluginId ? [...executable.filter(ofPlugin), ...executable.filter((t) => !ofPlugin(t))] : executable;
|
|
996
|
+
return ranked.slice(0, maxTools).map((tool) => ({
|
|
997
|
+
name: tool.name,
|
|
998
|
+
description: tool.description,
|
|
999
|
+
parameters: tool.parameters
|
|
1000
|
+
}));
|
|
1001
|
+
}
|
|
1002
|
+
async function resolveTeamRows(ctx) {
|
|
1003
|
+
if (ctx.loadTeam) {
|
|
1004
|
+
const team = await ctx.loadTeam();
|
|
1005
|
+
return team.map((p) => ({
|
|
1006
|
+
name: p.name ?? null,
|
|
1007
|
+
kind: p.kind,
|
|
1008
|
+
email: p.email ?? null,
|
|
1009
|
+
accessRole: p.membership?.profileName ?? null,
|
|
1010
|
+
hasLogin: !!p.membership
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
1013
|
+
return ctx.members.map((m) => ({
|
|
1014
|
+
name: m.user?.fullName ?? null,
|
|
1015
|
+
email: m.user?.email ?? null,
|
|
1016
|
+
accessRole: m.profileName ?? null,
|
|
1017
|
+
hasLogin: true
|
|
1018
|
+
}));
|
|
1019
|
+
}
|
|
1020
|
+
registerAIToolHandler("getBusinessSummary", async (_args, ctx) => {
|
|
1021
|
+
if (!ctx.currentOrg) return { error: "no_active_business" };
|
|
1022
|
+
const org = ctx.currentOrg;
|
|
1023
|
+
return {
|
|
1024
|
+
name: org.name,
|
|
1025
|
+
slug: org.slug,
|
|
1026
|
+
plan: org.plan ?? null,
|
|
1027
|
+
vertical: org.verticalId ?? null,
|
|
1028
|
+
teamSize: (await resolveTeamRows(ctx)).length,
|
|
1029
|
+
createdAt: org.createdAt ?? null
|
|
1030
|
+
};
|
|
1031
|
+
});
|
|
1032
|
+
registerAIToolHandler("getTeamMembers", async (args, ctx) => {
|
|
1033
|
+
const filter = typeof args.role === "string" ? args.role.trim().toLowerCase() : void 0;
|
|
1034
|
+
const rows = await resolveTeamRows(ctx);
|
|
1035
|
+
const members = filter ? rows.filter(
|
|
1036
|
+
(r) => String(r.kind ?? "").toLowerCase() === filter || String(r.accessRole ?? "").toLowerCase() === filter
|
|
1037
|
+
) : rows;
|
|
1038
|
+
return { total: members.length, members };
|
|
1039
|
+
});
|
|
1040
|
+
registerAIToolHandler("navigateTo", (args, ctx) => {
|
|
1041
|
+
const query = typeof args.page === "string" ? args.page.trim().toLowerCase() : "";
|
|
1042
|
+
if (!query) return { error: "missing_page" };
|
|
1043
|
+
const match = ctx.routes.find((r) => r.path.toLowerCase() === query || r.path.toLowerCase() === `/${query}`) ?? ctx.routes.find((r) => (r.label ?? "").toLowerCase() === query) ?? ctx.routes.find((r) => {
|
|
1044
|
+
const label = (r.label ?? "").toLowerCase();
|
|
1045
|
+
return !!label && (label.startsWith(query) || query.startsWith(label));
|
|
1046
|
+
});
|
|
1047
|
+
if (!match) {
|
|
1048
|
+
return {
|
|
1049
|
+
error: "page_not_found",
|
|
1050
|
+
message: "No page matches that. If the user meant a specific record, look it up and call openRecord with its id.",
|
|
1051
|
+
availablePages: ctx.routes.map((r) => r.label ?? r.path)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
ctx.navigate(match.path);
|
|
1055
|
+
revealPage();
|
|
1056
|
+
return { navigatedTo: match.path, label: match.label ?? match.path };
|
|
1057
|
+
});
|
|
1058
|
+
registerAIToolHandler("openRecord", async (args, ctx) => {
|
|
1059
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
1060
|
+
if (!id) return { error: "missing_id", message: "Provide the record id returned by an earlier tool." };
|
|
1061
|
+
if (ctx.currentPath.includes(id)) {
|
|
1062
|
+
return {
|
|
1063
|
+
alreadyOpen: true,
|
|
1064
|
+
message: "This record is already the page the user is looking at \u2014 nothing to navigate. To talk about it, read it with searchRecords filtered by this id."
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
const ref = lookupRecordRef(id);
|
|
1068
|
+
if (!ref) {
|
|
1069
|
+
return {
|
|
1070
|
+
error: "unknown_record",
|
|
1071
|
+
message: "That id was not seen in this conversation. Search for the record first, then call openRecord with the id from the result."
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
if (!resolveEntityRoute(ref.archetype, ref.kind, ref.entityKey)) {
|
|
1075
|
+
return { error: "no_page_for_record", message: `This app has no page for ${ref.label}.` };
|
|
1076
|
+
}
|
|
1077
|
+
openEntity(id, ref.archetype, ref.kind, void 0, ref.entityKey);
|
|
1078
|
+
revealPage();
|
|
1079
|
+
return { opened: ref.label, ref: { id, label: ref.label, archetype: ref.archetype } };
|
|
1080
|
+
});
|
|
1081
|
+
function validateGrants(features, raw) {
|
|
1082
|
+
const known = new Set(features.map((f) => f.id));
|
|
1083
|
+
const grants = {};
|
|
1084
|
+
const rejected = [];
|
|
1085
|
+
const isAction = (a) => PROFILE_ACTIONS.includes(a);
|
|
1086
|
+
for (const [featureId, value] of Object.entries(raw ?? {})) {
|
|
1087
|
+
if (!known.has(featureId)) {
|
|
1088
|
+
rejected.push(`${featureId}: not an area of this app`);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
const asked = (Array.isArray(value) ? value : [value]).map((a) => String(a).trim().toLowerCase());
|
|
1092
|
+
const unknown = asked.filter((a) => !isAction(a));
|
|
1093
|
+
if (unknown.length) rejected.push(`${featureId}: unknown action(s) ${unknown.join(", ")}`);
|
|
1094
|
+
const actions = asked.filter(isAction);
|
|
1095
|
+
if (actions.length) grants[featureId] = actions;
|
|
1096
|
+
}
|
|
1097
|
+
return { grants, rejected };
|
|
1098
|
+
}
|
|
1099
|
+
registerAIToolHandler("createPermissionProfile", async (args, ctx) => {
|
|
1100
|
+
const profile = usePermissionsStore.getState().currentProfile;
|
|
1101
|
+
if (!profileHasSystemPermission(profile, "manage_permissions")) {
|
|
1102
|
+
return {
|
|
1103
|
+
error: "forbidden",
|
|
1104
|
+
message: "This user cannot manage permissions. Say so \u2014 do not try another tool."
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
if (!ctx.orgAdapter?.createProfile || !ctx.currentOrg) {
|
|
1108
|
+
return {
|
|
1109
|
+
error: "not_executable",
|
|
1110
|
+
message: "This app has no permission-profile store wired. Tell the user to create the profile in Settings \u2192 Permissions."
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
1114
|
+
if (!name) return { error: "missing_name", message: "Give the profile a name." };
|
|
1115
|
+
const features = usePermissionsStore.getState().features;
|
|
1116
|
+
const byId = new Map(features.map((f) => [f.id, f]));
|
|
1117
|
+
const { grants, rejected } = validateGrants(features, args.grants);
|
|
1118
|
+
if (!Object.keys(grants).length) {
|
|
1119
|
+
return {
|
|
1120
|
+
error: "empty_grants",
|
|
1121
|
+
message: `No valid area was granted. Use the ids from the tool's area list and the actions ${PROFILE_ACTIONS.join("/")}.`,
|
|
1122
|
+
...rejected.length ? { rejected } : {},
|
|
1123
|
+
availableAreas: features.map((f) => f.id)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
const existing = usePermissionsStore.getState().profiles;
|
|
1127
|
+
const clash = existing.find((p) => p.name.trim().toLowerCase() === name.toLowerCase());
|
|
1128
|
+
if (clash) {
|
|
1129
|
+
return {
|
|
1130
|
+
error: "name_taken",
|
|
1131
|
+
message: `A profile named "${clash.name}" already exists. Ask the user for a different name, or whether they meant to change that one (which happens in Settings \u2192 Permissions).`
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
const summary = describeGrants(grants, byId);
|
|
1135
|
+
const decision = await requestChatConfirmation({
|
|
1136
|
+
id: `permission-profile-${Date.now()}`,
|
|
1137
|
+
toolName: "createPermissionProfile",
|
|
1138
|
+
title: `Criar perfil de acesso: ${name}`,
|
|
1139
|
+
params: {
|
|
1140
|
+
nome: name,
|
|
1141
|
+
...typeof args.description === "string" && args.description.trim() ? { descri\u00E7\u00E3o: args.description.trim() } : {},
|
|
1142
|
+
acessos: summary
|
|
1143
|
+
},
|
|
1144
|
+
plane: "client",
|
|
1145
|
+
noAutoApproval: true
|
|
1146
|
+
});
|
|
1147
|
+
if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
1148
|
+
const finalName = typeof decision.values?.nome === "string" && decision.values.nome.trim() ? decision.values.nome.trim() : name;
|
|
1149
|
+
const finalDescription = typeof decision.values?.["descri\xE7\xE3o"] === "string" ? decision.values["descri\xE7\xE3o"].trim() || void 0 : typeof args.description === "string" && args.description.trim() ? args.description.trim() : void 0;
|
|
1150
|
+
let created;
|
|
1151
|
+
try {
|
|
1152
|
+
created = await ctx.orgAdapter.createProfile(ctx.currentOrg.id, {
|
|
1153
|
+
name: finalName,
|
|
1154
|
+
description: finalDescription,
|
|
1155
|
+
grants
|
|
1156
|
+
});
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
return {
|
|
1159
|
+
error: "create_failed",
|
|
1160
|
+
message: `The profile could not be saved: ${describeThrown(error)}. Report this to the user \u2014 do not retry.`
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (ctx.orgAdapter.listProfiles) {
|
|
1164
|
+
try {
|
|
1165
|
+
usePermissionsStore.getState().setProfiles(await ctx.orgAdapter.listProfiles(ctx.currentOrg.id));
|
|
1166
|
+
} catch {
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
ok: true,
|
|
1171
|
+
profile: { id: created.id, name: created.name },
|
|
1172
|
+
grants: summary,
|
|
1173
|
+
...rejected.length ? { rejectedFromYourProposal: rejected } : {},
|
|
1174
|
+
message: "Profile created. Summarise for the user WHAT IT CAN REACH using the labels in `grants`, then say the profile is assigned to people on the Team screen. The link to review it is already in the reply.",
|
|
1175
|
+
ref: { id: created.id, label: created.name, path: PERMISSIONS_PATH }
|
|
1176
|
+
};
|
|
1177
|
+
});
|
|
1178
|
+
registerAIToolHandler("updatePermissionProfile", async (args, ctx) => {
|
|
1179
|
+
const actor = usePermissionsStore.getState().currentProfile;
|
|
1180
|
+
if (!profileHasSystemPermission(actor, "manage_permissions")) {
|
|
1181
|
+
return {
|
|
1182
|
+
error: "forbidden",
|
|
1183
|
+
message: "This user cannot manage permissions. Say so \u2014 do not try another tool."
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (!ctx.orgAdapter?.updateProfile || !ctx.currentOrg) {
|
|
1187
|
+
return {
|
|
1188
|
+
error: "not_executable",
|
|
1189
|
+
message: "This app cannot change profiles from here. Tell the user to edit it in Settings \u2192 Permissions."
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
1193
|
+
const { features, profiles } = usePermissionsStore.getState();
|
|
1194
|
+
const target = profiles.find((p) => p.id === id);
|
|
1195
|
+
if (!target) {
|
|
1196
|
+
return {
|
|
1197
|
+
error: "unknown_profile",
|
|
1198
|
+
message: `No profile with id "${id}". Existing: ${profiles.map((p) => `${p.id} (${p.name})`).join(", ")}.`
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (target.isSystem) {
|
|
1202
|
+
return {
|
|
1203
|
+
error: "system_profile",
|
|
1204
|
+
message: `"${target.name}" is a built-in role and cannot be changed. Offer to create a new profile based on it instead (createPermissionProfile).`
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const byId = new Map(features.map((f) => [f.id, f]));
|
|
1208
|
+
const { grants: setAreas, rejected } = validateGrants(features, args.grants);
|
|
1209
|
+
const revoke = (Array.isArray(args.revoke) ? args.revoke : []).map((a) => String(a).trim()).filter(Boolean);
|
|
1210
|
+
const before = target.grants ?? {};
|
|
1211
|
+
const next = { ...before, ...setAreas };
|
|
1212
|
+
for (const area of revoke) {
|
|
1213
|
+
if (!(area in next)) rejected.push(`${area}: was not granted, nothing to revoke`);
|
|
1214
|
+
delete next[area];
|
|
1215
|
+
}
|
|
1216
|
+
const rename = typeof args.name === "string" && args.name.trim() && args.name.trim() !== target.name ? args.name.trim() : void 0;
|
|
1217
|
+
const redescribe = typeof args.description === "string" && args.description.trim() !== (target.description ?? "") ? args.description.trim() : void 0;
|
|
1218
|
+
const same = (a, b) => !!a && !!b && a.length === b.length && a.every((x) => b.includes(x));
|
|
1219
|
+
const untouched = Object.keys(next).length === Object.keys(before).length && Object.keys(next).every((k) => same(next[k], before[k]));
|
|
1220
|
+
if (untouched && !rename && redescribe === void 0) {
|
|
1221
|
+
return {
|
|
1222
|
+
error: "no_change",
|
|
1223
|
+
message: "That would leave the profile exactly as it is. Tell the user what it already has instead of writing it again.",
|
|
1224
|
+
current: describeGrants(before, byId),
|
|
1225
|
+
...rejected.length ? { rejectedFromYourProposal: rejected } : {}
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
const rows = [];
|
|
1229
|
+
for (const area of Object.keys(next)) {
|
|
1230
|
+
const label = byId.get(area)?.label ?? area;
|
|
1231
|
+
const now = (next[area] ?? []).map((a) => ACTION_LABELS[a] ?? a).join(", ");
|
|
1232
|
+
if (!(area in before)) rows.push({ label, type: `${now} (novo)` });
|
|
1233
|
+
else if (!same(next[area], before[area])) {
|
|
1234
|
+
const was = (before[area] ?? []).map((a) => ACTION_LABELS[a] ?? a).join(", ");
|
|
1235
|
+
rows.push({ label, type: `${now} (antes: ${was})` });
|
|
1236
|
+
} else rows.push({ label, type: now });
|
|
1237
|
+
}
|
|
1238
|
+
for (const area of Object.keys(before)) {
|
|
1239
|
+
if (!(area in next)) rows.push({ label: byId.get(area)?.label ?? area, type: "REMOVIDO" });
|
|
1240
|
+
}
|
|
1241
|
+
const decision = await requestChatConfirmation({
|
|
1242
|
+
id: `permission-profile-update-${Date.now()}`,
|
|
1243
|
+
toolName: "updatePermissionProfile",
|
|
1244
|
+
title: `Alterar perfil de acesso: ${rename ?? target.name}`,
|
|
1245
|
+
params: {
|
|
1246
|
+
perfil: target.name,
|
|
1247
|
+
...rename ? { "novo nome": rename } : {},
|
|
1248
|
+
...redescribe !== void 0 ? { descri\u00E7\u00E3o: redescribe } : {},
|
|
1249
|
+
acessos: rows
|
|
1250
|
+
},
|
|
1251
|
+
plane: "client",
|
|
1252
|
+
noAutoApproval: true
|
|
1253
|
+
});
|
|
1254
|
+
if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
1255
|
+
const finalName = typeof decision.values?.["novo nome"] === "string" && decision.values["novo nome"] ? decision.values["novo nome"].trim() : rename;
|
|
1256
|
+
try {
|
|
1257
|
+
await ctx.orgAdapter.updateProfile(ctx.currentOrg.id, target.id, {
|
|
1258
|
+
...finalName ? { name: finalName } : {},
|
|
1259
|
+
...redescribe !== void 0 ? { description: redescribe || void 0 } : {},
|
|
1260
|
+
grants: next
|
|
1261
|
+
});
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
return {
|
|
1264
|
+
error: "update_failed",
|
|
1265
|
+
message: `The profile could not be saved: ${describeThrown(error)}. Report this to the user \u2014 do not retry.`
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (ctx.orgAdapter.listProfiles) {
|
|
1269
|
+
try {
|
|
1270
|
+
usePermissionsStore.getState().setProfiles(await ctx.orgAdapter.listProfiles(ctx.currentOrg.id));
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
ok: true,
|
|
1276
|
+
profile: { id: target.id, name: finalName ?? target.name },
|
|
1277
|
+
grants: describeGrants(next, byId),
|
|
1278
|
+
...rejected.length ? { rejectedFromYourProposal: rejected } : {},
|
|
1279
|
+
message: "Profile updated. Tell the user WHAT CHANGED and what the profile can reach now, using the labels in `grants` \u2014 not the ids. The link to review it is already in the reply.",
|
|
1280
|
+
ref: { id: target.id, label: finalName ?? target.name, path: PERMISSIONS_PATH }
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
function describeGrants(grants, byId) {
|
|
1284
|
+
return Object.entries(grants).map(([featureId, actions]) => ({
|
|
1285
|
+
label: byId.get(featureId)?.label ?? featureId,
|
|
1286
|
+
type: actions.map((a) => ACTION_LABELS[a] ?? a).join(", ")
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
var PERMISSIONS_PATH = "/settings/permissions";
|
|
1290
|
+
var ACTION_LABELS = {
|
|
1291
|
+
read: "ver",
|
|
1292
|
+
create: "criar",
|
|
1293
|
+
edit: "editar",
|
|
1294
|
+
delete: "excluir"
|
|
1295
|
+
};
|
|
1296
|
+
async function executeRpcTool(rpcName, args, userId) {
|
|
1297
|
+
const supabase = getSupabaseClientOptional();
|
|
1298
|
+
const tenantId = getActiveTenantId();
|
|
1299
|
+
if (!supabase?.rpc || !tenantId || !userId) {
|
|
1300
|
+
return errorResult("not_executable", "No authenticated session/tenant available for this action.");
|
|
1301
|
+
}
|
|
1302
|
+
const { data, error } = await supabase.rpc(rpcName, {
|
|
1303
|
+
p_tenant_id: tenantId,
|
|
1304
|
+
p_actor_user_id: userId,
|
|
1305
|
+
p_payload: args
|
|
1306
|
+
});
|
|
1307
|
+
if (error) return errorResult("execution_failed", error.message);
|
|
1308
|
+
return { ok: true, content: JSON.stringify(data ?? { ok: true }) };
|
|
1309
|
+
}
|
|
1310
|
+
registerAIToolHandler(
|
|
1311
|
+
"findTools",
|
|
1312
|
+
(args, ctx) => resolveToolSearch(args, ctx.searchableTools ?? [], new Set(ctx.carriedTools ?? []))
|
|
1313
|
+
);
|
|
1314
|
+
function useAgentSurface(contribution) {
|
|
1315
|
+
const ref = React.useRef(contribution);
|
|
1316
|
+
ref.current = contribution;
|
|
1317
|
+
const id = contribution.id;
|
|
1318
|
+
React.useEffect(() => {
|
|
1319
|
+
registerSurface({ id, get: () => ref.current });
|
|
1320
|
+
const names = (ref.current.tools ?? []).map((t) => t.tool.name);
|
|
1321
|
+
for (const name of names) {
|
|
1322
|
+
registerAIToolHandler(name, (args) => {
|
|
1323
|
+
const entry = ref.current.tools?.find((t) => t.tool.name === name);
|
|
1324
|
+
if (!entry) return { error: "tool_unmounted", message: "That surface is no longer open." };
|
|
1325
|
+
return entry.execute(args);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return () => {
|
|
1329
|
+
unregisterSurface(id);
|
|
1330
|
+
for (const name of names) unregisterAIToolHandler(name);
|
|
1331
|
+
};
|
|
1332
|
+
}, [id]);
|
|
1333
|
+
}
|
|
1334
|
+
var HANDOFF_ID = ASK_AI_HANDOFF_ID;
|
|
1335
|
+
var releaseIntro = null;
|
|
1336
|
+
function borrowIntro(intro, placeholder) {
|
|
1337
|
+
const store = useAssistantInviteStore.getState();
|
|
1338
|
+
const previous = store.invite?.id === HANDOFF_ID ? null : store.invite ?? null;
|
|
1339
|
+
if (previous || !releaseIntro) {
|
|
1340
|
+
releaseIntro = () => {
|
|
1341
|
+
if (useAssistantInviteStore.getState().invite?.id === HANDOFF_ID) {
|
|
1342
|
+
useAssistantInviteStore.getState().setInvite(previous);
|
|
1343
|
+
}
|
|
1344
|
+
releaseIntro = null;
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
const def = {
|
|
1348
|
+
id: HANDOFF_ID,
|
|
1349
|
+
...intro.role ? { role: intro.role } : {},
|
|
1350
|
+
persona: intro.persona,
|
|
1351
|
+
headline: intro.pitch,
|
|
1352
|
+
pitch: intro.pitch,
|
|
1353
|
+
description: intro.description,
|
|
1354
|
+
placeholder,
|
|
1355
|
+
suggestions: intro.suggestions,
|
|
1356
|
+
// Never the hero takeover: the user asked for the assistant from a screen
|
|
1357
|
+
// they are still looking at, and covering it would be answering a question
|
|
1358
|
+
// they did not ask.
|
|
1359
|
+
auto: false
|
|
1360
|
+
};
|
|
1361
|
+
store.setInvite(def);
|
|
1362
|
+
}
|
|
1363
|
+
function releaseOnClose() {
|
|
1364
|
+
const stop = useChatStore.subscribe((state, previous) => {
|
|
1365
|
+
if (previous.isOpen && !state.isOpen) {
|
|
1366
|
+
releaseIntro?.();
|
|
1367
|
+
stop();
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
function askAI({ hint, prompt, draft, media, intro } = {}) {
|
|
1372
|
+
const chat = useChatStore.getState();
|
|
1373
|
+
chat.setComposerHint(hint ?? null);
|
|
1374
|
+
chat.requestComposerFocus();
|
|
1375
|
+
if (media) chat.attachMedia(media);
|
|
1376
|
+
if (draft) chat.setDraft(draft);
|
|
1377
|
+
if (intro) {
|
|
1378
|
+
borrowIntro(intro, hint);
|
|
1379
|
+
releaseOnClose();
|
|
1380
|
+
}
|
|
1381
|
+
if (prompt) chat.queuePrompt(prompt);
|
|
1382
|
+
else chat.setOpen(true);
|
|
1383
|
+
}
|
|
1384
|
+
function AskAIMenuItem({
|
|
1385
|
+
hint,
|
|
1386
|
+
prompt,
|
|
1387
|
+
draft,
|
|
1388
|
+
media,
|
|
1389
|
+
intro,
|
|
1390
|
+
label,
|
|
1391
|
+
description,
|
|
1392
|
+
onHandoff
|
|
1393
|
+
}) {
|
|
1394
|
+
const t = useTranslation();
|
|
1395
|
+
return /* @__PURE__ */ jsxs(
|
|
1396
|
+
DropdownItem,
|
|
1397
|
+
{
|
|
1398
|
+
onSelect: () => {
|
|
1399
|
+
askAI({ hint, prompt, draft, media, intro });
|
|
1400
|
+
onHandoff?.();
|
|
1401
|
+
},
|
|
1402
|
+
className: "fz-ai-action items-start gap-2 py-2",
|
|
1403
|
+
children: [
|
|
1404
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
|
|
1405
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
|
|
1406
|
+
/* @__PURE__ */ jsx("span", { className: "block font-medium", children: label ?? t("assistant.askAI") }),
|
|
1407
|
+
description ? /* @__PURE__ */ jsx("span", { className: "block text-xs font-normal opacity-70", children: description }) : null
|
|
1408
|
+
] })
|
|
1409
|
+
]
|
|
1410
|
+
}
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
function AskAIButton({
|
|
1414
|
+
hint,
|
|
1415
|
+
prompt,
|
|
1416
|
+
draft,
|
|
1417
|
+
media,
|
|
1418
|
+
intro,
|
|
1419
|
+
label,
|
|
1420
|
+
onHandoff,
|
|
1421
|
+
size = "sm",
|
|
1422
|
+
variant = "default",
|
|
1423
|
+
className,
|
|
1424
|
+
disabled
|
|
1425
|
+
}) {
|
|
1426
|
+
const t = useTranslation();
|
|
1427
|
+
return /* @__PURE__ */ jsxs(
|
|
1428
|
+
Button,
|
|
1429
|
+
{
|
|
1430
|
+
type: "button",
|
|
1431
|
+
size,
|
|
1432
|
+
variant,
|
|
1433
|
+
className,
|
|
1434
|
+
disabled,
|
|
1435
|
+
onClick: () => {
|
|
1436
|
+
askAI({ hint, prompt, draft, media, intro });
|
|
1437
|
+
onHandoff?.();
|
|
1438
|
+
},
|
|
1439
|
+
children: [
|
|
1440
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
1441
|
+
label ?? t("assistant.askAI")
|
|
1442
|
+
]
|
|
1443
|
+
}
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
function DeleteConfirmDialog({ open, onClose, onConfirm, entityName, displayValue, mode = "delete" }) {
|
|
1447
|
+
const t = useTranslation();
|
|
1448
|
+
const soft = mode === "deactivate";
|
|
1449
|
+
const key = (suffix) => soft ? `crud.deactivate.${suffix}` : `crud.delete.${suffix}`;
|
|
1450
|
+
return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (isOpen) => {
|
|
1451
|
+
if (!isOpen) onClose();
|
|
1452
|
+
}, children: /* @__PURE__ */ jsxs(ModalContent, { children: [
|
|
1453
|
+
/* @__PURE__ */ jsxs(ModalHeader, { children: [
|
|
1454
|
+
/* @__PURE__ */ jsx(ModalTitle, { children: t(key("title"), { entity: entityName }) }),
|
|
1455
|
+
/* @__PURE__ */ jsx(ModalDescription, { children: displayValue ? t(key("confirmNamed"), { displayValue }) : t(key("confirmGeneric"), { entity: entityName.toLowerCase() }) })
|
|
1456
|
+
] }),
|
|
1457
|
+
/* @__PURE__ */ jsxs(ModalFooter, { children: [
|
|
1458
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("common.cancel") }),
|
|
1459
|
+
/* @__PURE__ */ jsx(Button, { variant: soft ? "default" : "destructive", onClick: onConfirm, children: soft ? t("crud.deactivate.action") : t("common.delete") })
|
|
1460
|
+
] })
|
|
1461
|
+
] }) });
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
export { ASK_AI_HANDOFF_ID, AskAIButton, AskAIMenuItem, DeleteConfirmDialog, askAI, buildAgentToolset, buildDataToolIndex, executeAITool, executeRpcTool, getActiveSurfaceContext, getSurfaceTools, getSurfaceVersion, hasAIToolHandler, registerAIToolHandler, revealPage, subscribeSurfaces, unregisterAIToolHandler, useAgentSurface, useAssistantInviteStore };
|
|
1465
|
+
//# sourceMappingURL=chunk-6QFOE3UD.js.map
|
|
1466
|
+
//# sourceMappingURL=chunk-6QFOE3UD.js.map
|