@open-mercato/ui 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -1
- package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +204 -0
- package/dist/ai/AiAssistantLauncher.js +596 -0
- package/dist/ai/AiAssistantLauncher.js.map +7 -0
- package/dist/ai/AiChat.js +1092 -0
- package/dist/ai/AiChat.js.map +7 -0
- package/dist/ai/AiChatSessions.js +297 -0
- package/dist/ai/AiChatSessions.js.map +7 -0
- package/dist/ai/AiDock.js +347 -0
- package/dist/ai/AiDock.js.map +7 -0
- package/dist/ai/AiMessageContent.js +369 -0
- package/dist/ai/AiMessageContent.js.map +7 -0
- package/dist/ai/ChatPaneTabs.js +251 -0
- package/dist/ai/ChatPaneTabs.js.map +7 -0
- package/dist/ai/index.js +115 -0
- package/dist/ai/index.js.map +7 -0
- package/dist/ai/parts/ConfirmationCard.js +211 -0
- package/dist/ai/parts/ConfirmationCard.js.map +7 -0
- package/dist/ai/parts/FieldDiffCard.js +119 -0
- package/dist/ai/parts/FieldDiffCard.js.map +7 -0
- package/dist/ai/parts/MutationPreviewCard.js +224 -0
- package/dist/ai/parts/MutationPreviewCard.js.map +7 -0
- package/dist/ai/parts/MutationResultCard.js +240 -0
- package/dist/ai/parts/MutationResultCard.js.map +7 -0
- package/dist/ai/parts/approval-cards-map.js +15 -0
- package/dist/ai/parts/approval-cards-map.js.map +7 -0
- package/dist/ai/parts/index.js +24 -0
- package/dist/ai/parts/index.js.map +7 -0
- package/dist/ai/parts/pending-action-api.js +60 -0
- package/dist/ai/parts/pending-action-api.js.map +7 -0
- package/dist/ai/parts/types.js +1 -0
- package/dist/ai/parts/types.js.map +7 -0
- package/dist/ai/parts/useAiPendingActionPolling.js +126 -0
- package/dist/ai/parts/useAiPendingActionPolling.js.map +7 -0
- package/dist/ai/records/ActivityCard.js +83 -0
- package/dist/ai/records/ActivityCard.js.map +7 -0
- package/dist/ai/records/CompanyCard.js +81 -0
- package/dist/ai/records/CompanyCard.js.map +7 -0
- package/dist/ai/records/DealCard.js +76 -0
- package/dist/ai/records/DealCard.js.map +7 -0
- package/dist/ai/records/PersonCard.js +68 -0
- package/dist/ai/records/PersonCard.js.map +7 -0
- package/dist/ai/records/ProductCard.js +68 -0
- package/dist/ai/records/ProductCard.js.map +7 -0
- package/dist/ai/records/RecordCard.js +29 -0
- package/dist/ai/records/RecordCard.js.map +7 -0
- package/dist/ai/records/RecordCardShell.js +103 -0
- package/dist/ai/records/RecordCardShell.js.map +7 -0
- package/dist/ai/records/index.js +31 -0
- package/dist/ai/records/index.js.map +7 -0
- package/dist/ai/records/registry.js +51 -0
- package/dist/ai/records/registry.js.map +7 -0
- package/dist/ai/records/types.js +1 -0
- package/dist/ai/records/types.js.map +7 -0
- package/dist/ai/ui-part-registry.js +112 -0
- package/dist/ai/ui-part-registry.js.map +7 -0
- package/dist/ai/ui-part-slots.js +14 -0
- package/dist/ai/ui-part-slots.js.map +7 -0
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +35 -0
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +7 -0
- package/dist/ai/upload-adapter.js +256 -0
- package/dist/ai/upload-adapter.js.map +7 -0
- package/dist/ai/useAiChat.js +549 -0
- package/dist/ai/useAiChat.js.map +7 -0
- package/dist/ai/useAiChatUpload.js +127 -0
- package/dist/ai/useAiChatUpload.js.map +7 -0
- package/dist/ai/useAiShortcuts.js +43 -0
- package/dist/ai/useAiShortcuts.js.map +7 -0
- package/dist/backend/AppShell.js +8 -4
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/BackendChromeProvider.js +2 -0
- package/dist/backend/BackendChromeProvider.js.map +2 -2
- package/dist/backend/DataTable.js +19 -2
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FilterBar.js +19 -15
- package/dist/backend/FilterBar.js.map +2 -2
- package/dist/backend/dashboard/DashboardScreen.js +31 -3
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/injection/spotIds.js +6 -0
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/notifications/useNotificationEffect.js +38 -2
- package/dist/backend/notifications/useNotificationEffect.js.map +2 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +2 -2
- package/jest.config.cjs +7 -1
- package/jest.markdown-mock.tsx +7 -0
- package/package.json +10 -4
- package/src/ai/AiAssistantLauncher.tsx +805 -0
- package/src/ai/AiChat.tsx +1483 -0
- package/src/ai/AiChatSessions.tsx +429 -0
- package/src/ai/AiDock.tsx +505 -0
- package/src/ai/AiMessageContent.tsx +515 -0
- package/src/ai/ChatPaneTabs.tsx +310 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +160 -0
- package/src/ai/__tests__/AiChat.debug.test.tsx +152 -0
- package/src/ai/__tests__/AiChat.registry.test.tsx +213 -0
- package/src/ai/__tests__/AiChat.test.tsx +257 -0
- package/src/ai/__tests__/AiDock.test.tsx +124 -0
- package/src/ai/__tests__/AiMessageContent.test.ts +111 -0
- package/src/ai/__tests__/ui-part-registry.test.ts +199 -0
- package/src/ai/__tests__/ui-part-slots.test.ts +43 -0
- package/src/ai/__tests__/upload-adapter.test.ts +213 -0
- package/src/ai/__tests__/useAiChatUpload.test.tsx +163 -0
- package/src/ai/__tests__/useAiShortcuts.test.tsx +100 -0
- package/src/ai/index.ts +125 -0
- package/src/ai/parts/ConfirmationCard.tsx +310 -0
- package/src/ai/parts/FieldDiffCard.tsx +173 -0
- package/src/ai/parts/MutationPreviewCard.tsx +302 -0
- package/src/ai/parts/MutationResultCard.tsx +360 -0
- package/src/ai/parts/__tests__/ConfirmationCard.test.tsx +169 -0
- package/src/ai/parts/__tests__/FieldDiffCard.test.tsx +74 -0
- package/src/ai/parts/__tests__/MutationPreviewCard.test.tsx +177 -0
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +127 -0
- package/src/ai/parts/__tests__/useAiPendingActionPolling.test.tsx +151 -0
- package/src/ai/parts/approval-cards-map.ts +24 -0
- package/src/ai/parts/index.ts +27 -0
- package/src/ai/parts/pending-action-api.ts +123 -0
- package/src/ai/parts/types.ts +84 -0
- package/src/ai/parts/useAiPendingActionPolling.ts +210 -0
- package/src/ai/records/ActivityCard.tsx +102 -0
- package/src/ai/records/CompanyCard.tsx +89 -0
- package/src/ai/records/DealCard.tsx +85 -0
- package/src/ai/records/PersonCard.tsx +77 -0
- package/src/ai/records/ProductCard.tsx +83 -0
- package/src/ai/records/RecordCard.tsx +37 -0
- package/src/ai/records/RecordCardShell.tsx +169 -0
- package/src/ai/records/index.ts +30 -0
- package/src/ai/records/registry.tsx +80 -0
- package/src/ai/records/types.ts +90 -0
- package/src/ai/ui-part-registry.ts +233 -0
- package/src/ai/ui-part-slots.ts +32 -0
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +50 -0
- package/src/ai/upload-adapter.ts +421 -0
- package/src/ai/useAiChat.ts +865 -0
- package/src/ai/useAiChatUpload.ts +180 -0
- package/src/ai/useAiShortcuts.ts +79 -0
- package/src/backend/AppShell.tsx +12 -5
- package/src/backend/BackendChromeProvider.tsx +2 -0
- package/src/backend/DataTable.tsx +20 -1
- package/src/backend/FilterBar.tsx +26 -13
- package/src/backend/__tests__/BackendChromeProvider.test.tsx +45 -0
- package/src/backend/dashboard/DashboardScreen.tsx +38 -3
- package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +24 -1
- package/src/backend/injection/spotIds.ts +6 -0
- package/src/backend/notifications/__tests__/useNotificationEffect.test.tsx +77 -0
- package/src/backend/notifications/useNotificationEffect.ts +47 -2
- package/src/index.ts +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AlertTriangle, CheckCircle2, Wand2, XCircle } from "lucide-react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { Alert, AlertTitle } from "../../primitives/alert.js";
|
|
6
|
+
import { Button } from "../../primitives/button.js";
|
|
7
|
+
import { useAiPendingActionPolling } from "./useAiPendingActionPolling.js";
|
|
8
|
+
const AI_CHAT_FIX_REQUEST_EVENT = "om-ai-chat-fix-request";
|
|
9
|
+
function dispatchFixRequest(detail) {
|
|
10
|
+
if (typeof window === "undefined") return;
|
|
11
|
+
window.dispatchEvent(new CustomEvent(AI_CHAT_FIX_REQUEST_EVENT, { detail }));
|
|
12
|
+
}
|
|
13
|
+
function isSuccessStatus(status) {
|
|
14
|
+
return status === "confirmed";
|
|
15
|
+
}
|
|
16
|
+
function isFailureStatus(status) {
|
|
17
|
+
return status === "failed" || status === "cancelled" || status === "expired";
|
|
18
|
+
}
|
|
19
|
+
function MutationResultCard(props) {
|
|
20
|
+
const t = useT();
|
|
21
|
+
const pendingActionId = props.pendingActionId ?? "";
|
|
22
|
+
const payload = props.payload ?? {};
|
|
23
|
+
const injected = props.initialAction ?? payload.pendingAction ?? null;
|
|
24
|
+
const { action: polled } = useAiPendingActionPolling({
|
|
25
|
+
pendingActionId,
|
|
26
|
+
endpoint: props.endpoint,
|
|
27
|
+
disabled: !pendingActionId || Boolean(injected)
|
|
28
|
+
});
|
|
29
|
+
const action = injected ?? polled;
|
|
30
|
+
const status = action?.status ?? null;
|
|
31
|
+
const failedRecords = action?.failedRecords ?? null;
|
|
32
|
+
const result = action?.executionResult ?? null;
|
|
33
|
+
if (!action) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {
|
|
37
|
+
return /* @__PURE__ */ jsxs(Alert, { variant: "warning", "data-ai-mutation-result": "partial", children: [
|
|
38
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "size-4", "aria-hidden": true }),
|
|
39
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t(
|
|
40
|
+
"ai_assistant.chat.mutation_cards.result.partialTitle",
|
|
41
|
+
"Action applied with failures"
|
|
42
|
+
) }),
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm leading-relaxed", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("p", { children: t(
|
|
45
|
+
"ai_assistant.chat.mutation_cards.result.partialBody",
|
|
46
|
+
"Some records could not be updated."
|
|
47
|
+
) }),
|
|
48
|
+
/* @__PURE__ */ jsx(
|
|
49
|
+
"ul",
|
|
50
|
+
{
|
|
51
|
+
className: "mt-2 list-disc space-y-1 pl-5 text-xs",
|
|
52
|
+
"data-ai-mutation-failed-records": true,
|
|
53
|
+
children: failedRecords.map((record) => /* @__PURE__ */ jsxs("li", { "data-ai-mutation-failed-record": record.recordId, children: [
|
|
54
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: record.recordId }),
|
|
55
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1 text-muted-foreground", children: "\u2022" }),
|
|
56
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: record.error.code }),
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1 text-muted-foreground", children: "\u2014" }),
|
|
58
|
+
/* @__PURE__ */ jsx("span", { children: record.error.message })
|
|
59
|
+
] }, record.recordId))
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
] })
|
|
63
|
+
] });
|
|
64
|
+
}
|
|
65
|
+
if (isSuccessStatus(status)) {
|
|
66
|
+
const recordId = result?.recordId ?? action.targetRecordId ?? null;
|
|
67
|
+
const href = payload.recordHref ?? null;
|
|
68
|
+
return /* @__PURE__ */ jsxs(Alert, { variant: "success", "data-ai-mutation-result": "success", children: [
|
|
69
|
+
/* @__PURE__ */ jsx(CheckCircle2, { className: "size-4", "aria-hidden": true }),
|
|
70
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.chat.mutation_cards.result.successTitle", "Action applied") }),
|
|
71
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm leading-relaxed", children: [
|
|
72
|
+
/* @__PURE__ */ jsx("p", { children: result?.commandName ? t(
|
|
73
|
+
"ai_assistant.chat.mutation_cards.result.successWithCommand",
|
|
74
|
+
"Completed"
|
|
75
|
+
) + `: ${result.commandName}` : t(
|
|
76
|
+
"ai_assistant.chat.mutation_cards.result.successBody",
|
|
77
|
+
"The mutation completed successfully."
|
|
78
|
+
) }),
|
|
79
|
+
recordId ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs", children: href ? /* @__PURE__ */ jsxs(
|
|
80
|
+
"a",
|
|
81
|
+
{
|
|
82
|
+
className: "font-mono text-primary underline",
|
|
83
|
+
href,
|
|
84
|
+
"data-ai-mutation-result-link": true,
|
|
85
|
+
children: [
|
|
86
|
+
t(
|
|
87
|
+
"ai_assistant.chat.mutation_cards.result.viewRecord",
|
|
88
|
+
"View record"
|
|
89
|
+
),
|
|
90
|
+
": ",
|
|
91
|
+
recordId
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
) : /* @__PURE__ */ jsx("span", { className: "font-mono", "data-ai-mutation-result-record-id": true, children: recordId }) }) : null
|
|
95
|
+
] })
|
|
96
|
+
] });
|
|
97
|
+
}
|
|
98
|
+
if (isFailureStatus(status) || result?.error) {
|
|
99
|
+
const code = result?.error?.code ?? status ?? "failed";
|
|
100
|
+
const message = result?.error?.message ?? t(
|
|
101
|
+
"ai_assistant.chat.mutation_cards.result.failureBody",
|
|
102
|
+
"The mutation could not be applied."
|
|
103
|
+
);
|
|
104
|
+
const errorObj = result?.error;
|
|
105
|
+
const errorDetails = errorObj?.details;
|
|
106
|
+
const errorInput = errorObj?.input;
|
|
107
|
+
const errorName = errorObj?.name;
|
|
108
|
+
const onFixWithAi = () => {
|
|
109
|
+
const promptLines = [
|
|
110
|
+
`The previous call to tool "${action.toolName}" failed.`,
|
|
111
|
+
`Error: ${code} \u2014 ${message}.`
|
|
112
|
+
];
|
|
113
|
+
if (errorName && errorName !== "Error") {
|
|
114
|
+
promptLines.push(`Error class: ${errorName}.`);
|
|
115
|
+
}
|
|
116
|
+
if (action.targetEntityType || action.targetRecordId) {
|
|
117
|
+
promptLines.push(
|
|
118
|
+
`Target: ${action.targetEntityType ?? "?"}${action.targetRecordId ? " / " + action.targetRecordId : ""}.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const issues = errorDetails?.issues ?? [];
|
|
122
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
123
|
+
promptLines.push("", "Validation issues:");
|
|
124
|
+
for (const issue of issues) {
|
|
125
|
+
const path = Array.isArray(issue?.path) && issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
126
|
+
const msg = issue?.message ?? "(no message)";
|
|
127
|
+
const codeHint = issue?.code ? ` [${issue.code}]` : "";
|
|
128
|
+
const expHint = issue?.expected || issue?.received ? ` (expected ${issue?.expected ?? "?"}, got ${issue?.received ?? "?"})` : "";
|
|
129
|
+
promptLines.push(`- ${path}: ${msg}${codeHint}${expHint}`);
|
|
130
|
+
}
|
|
131
|
+
} else if (errorDetails?.fieldErrors && typeof errorDetails.fieldErrors === "object") {
|
|
132
|
+
const entries = Object.entries(errorDetails.fieldErrors);
|
|
133
|
+
if (entries.length > 0) {
|
|
134
|
+
promptLines.push("", "Field errors:");
|
|
135
|
+
for (const [path, msgs] of entries) {
|
|
136
|
+
const list = Array.isArray(msgs) ? msgs.join("; ") : String(msgs);
|
|
137
|
+
promptLines.push(`- ${path}: ${list}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (errorInput !== void 0) {
|
|
142
|
+
try {
|
|
143
|
+
const json = JSON.stringify(errorInput, null, 2);
|
|
144
|
+
if (json && json !== "{}" && json.length <= 4e3) {
|
|
145
|
+
promptLines.push("", "Arguments you sent:", "```json", json, "```");
|
|
146
|
+
} else if (json && json.length > 4e3) {
|
|
147
|
+
promptLines.push(
|
|
148
|
+
"",
|
|
149
|
+
"Arguments you sent (truncated):",
|
|
150
|
+
"```json",
|
|
151
|
+
json.slice(0, 4e3) + "\n\u2026 [truncated]",
|
|
152
|
+
"```"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (errorDetails?.cause !== void 0) {
|
|
159
|
+
try {
|
|
160
|
+
const causeJson = JSON.stringify(errorDetails.cause, null, 2);
|
|
161
|
+
if (causeJson && causeJson !== "{}" && causeJson.length <= 1500) {
|
|
162
|
+
promptLines.push("", "Underlying cause:", "```json", causeJson, "```");
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (failedRecords && failedRecords.length > 0) {
|
|
168
|
+
promptLines.push("", "Records that failed:");
|
|
169
|
+
for (const rec of failedRecords) {
|
|
170
|
+
promptLines.push(
|
|
171
|
+
`- ${rec.recordId} \u2192 ${rec.error.code}: ${rec.error.message}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
promptLines.push(
|
|
176
|
+
"",
|
|
177
|
+
"Diagnose what went wrong using the validation issues / cause / arguments above, correct the arguments, and call the tool again. If the failure indicates missing prerequisites (e.g. a deal needs a linked person/company before commenting), tell me what to fix on the platform side instead of retrying blindly. Do not repeat the exact same arguments \u2014 you must change at least one parameter or stop and explain."
|
|
178
|
+
);
|
|
179
|
+
dispatchFixRequest({
|
|
180
|
+
message: promptLines.join("\n"),
|
|
181
|
+
toolName: action.toolName,
|
|
182
|
+
pendingActionId: action.id
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
const visibleIssues = Array.isArray(errorDetails?.issues) ? errorDetails.issues.filter((entry) => entry && (entry.message || entry.path)) : [];
|
|
186
|
+
return /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-mutation-result": "failure", children: [
|
|
187
|
+
/* @__PURE__ */ jsx(XCircle, { className: "size-4", "aria-hidden": true }),
|
|
188
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t(
|
|
189
|
+
"ai_assistant.chat.mutation_cards.result.failureTitle",
|
|
190
|
+
"Action failed"
|
|
191
|
+
) }),
|
|
192
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm leading-relaxed", children: [
|
|
193
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
194
|
+
/* @__PURE__ */ jsx("span", { className: "mr-2 font-mono text-xs", "data-ai-mutation-result-code": true, children: code }),
|
|
195
|
+
/* @__PURE__ */ jsx("span", { children: message })
|
|
196
|
+
] }),
|
|
197
|
+
visibleIssues.length > 0 ? /* @__PURE__ */ jsx(
|
|
198
|
+
"ul",
|
|
199
|
+
{
|
|
200
|
+
className: "mt-2 list-disc space-y-0.5 pl-5 text-xs",
|
|
201
|
+
"data-ai-mutation-result-issues": true,
|
|
202
|
+
children: visibleIssues.map((issue, index) => {
|
|
203
|
+
const path = Array.isArray(issue?.path) && issue.path.length > 0 ? issue.path.join(".") : null;
|
|
204
|
+
return /* @__PURE__ */ jsxs("li", { children: [
|
|
205
|
+
path ? /* @__PURE__ */ jsx("span", { className: "font-mono", children: path }) : null,
|
|
206
|
+
path && issue?.message ? /* @__PURE__ */ jsx("span", { className: "mx-1", children: "\u2014" }) : null,
|
|
207
|
+
issue?.message ? /* @__PURE__ */ jsx("span", { children: issue.message }) : null
|
|
208
|
+
] }, index);
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
) : null,
|
|
212
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 flex items-center gap-2", children: /* @__PURE__ */ jsxs(
|
|
213
|
+
Button,
|
|
214
|
+
{
|
|
215
|
+
type: "button",
|
|
216
|
+
variant: "outline",
|
|
217
|
+
size: "sm",
|
|
218
|
+
onClick: onFixWithAi,
|
|
219
|
+
"data-ai-mutation-result-fix": true,
|
|
220
|
+
children: [
|
|
221
|
+
/* @__PURE__ */ jsx(Wand2, { className: "size-4", "aria-hidden": true }),
|
|
222
|
+
/* @__PURE__ */ jsx("span", { children: t(
|
|
223
|
+
"ai_assistant.chat.mutation_cards.result.fixWithAi",
|
|
224
|
+
"Fix with AI"
|
|
225
|
+
) })
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
) })
|
|
229
|
+
] })
|
|
230
|
+
] });
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
var MutationResultCard_default = MutationResultCard;
|
|
235
|
+
export {
|
|
236
|
+
AI_CHAT_FIX_REQUEST_EVENT,
|
|
237
|
+
MutationResultCard,
|
|
238
|
+
MutationResultCard_default as default
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=MutationResultCard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/ai/parts/MutationResultCard.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle, CheckCircle2, Wand2, XCircle } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Alert, AlertDescription, AlertTitle } from '../../primitives/alert'\nimport { Button } from '../../primitives/button'\nimport type { AiUiPartProps } from '../ui-part-registry'\nimport { useAiPendingActionPolling } from './useAiPendingActionPolling'\nimport type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'\n\n/** Custom DOM event the failure card dispatches when the operator clicks\n * \"Fix with AI\". `<AiChat>` listens for this and sends a follow-up user\n * message asking the agent to diagnose and retry the failed call. */\nexport const AI_CHAT_FIX_REQUEST_EVENT = 'om-ai-chat-fix-request'\n\nexport interface AiChatFixRequestDetail {\n message: string\n toolName?: string\n pendingActionId?: string\n}\n\nfunction dispatchFixRequest(detail: AiChatFixRequestDetail): void {\n if (typeof window === 'undefined') return\n window.dispatchEvent(new CustomEvent<AiChatFixRequestDetail>(AI_CHAT_FIX_REQUEST_EVENT, { detail }))\n}\n\n/**\n * Terminal-state card that renders the `executionResult` of a pending\n * action. Success \u2192 `Alert variant=\"success\"` with a record link; partial\n * success (batch `failedRecords[]`) \u2192 `variant=\"warning\"` with the list;\n * failure \u2192 `variant=\"destructive\"` with the error code + message.\n *\n * Reads the pending action via the shared polling hook so page reloads\n * still recover state. The hook short-circuits once the row is terminal\n * (spec's reconnect behavior), which is always the case for this card.\n */\nexport interface MutationResultCardPayload {\n /** Server-serialized pending action snapshot (optional \u2014 the hook refetches). */\n pendingAction?: AiPendingActionCardAction\n /** Optional link target for the success record. */\n recordHref?: string\n}\n\nexport interface MutationResultCardProps extends AiUiPartProps {\n /** Optional injected action for tests \u2014 bypasses the polling fetch. */\n initialAction?: AiPendingActionCardAction\n /** Poll endpoint override for tests. */\n endpoint?: string\n}\n\nfunction isSuccessStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'confirmed'\n}\n\nfunction isFailureStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'failed' || status === 'cancelled' || status === 'expired'\n}\n\nexport function MutationResultCard(props: MutationResultCardProps) {\n const t = useT()\n const pendingActionId = props.pendingActionId ?? ''\n const payload = (props.payload as MutationResultCardPayload | undefined) ?? {}\n const injected = props.initialAction ?? payload.pendingAction ?? null\n\n const { action: polled } = useAiPendingActionPolling({\n pendingActionId,\n endpoint: props.endpoint,\n disabled: !pendingActionId || Boolean(injected),\n })\n const action = injected ?? polled\n const status = action?.status ?? null\n const failedRecords = action?.failedRecords ?? null\n const result = action?.executionResult ?? null\n\n if (!action) {\n return null\n }\n\n if (isSuccessStatus(status) && failedRecords && failedRecords.length > 0) {\n return (\n <Alert variant=\"warning\" data-ai-mutation-result=\"partial\">\n <AlertTriangle className=\"size-4\" aria-hidden />\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialTitle',\n 'Action applied with failures',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {t(\n 'ai_assistant.chat.mutation_cards.result.partialBody',\n 'Some records could not be updated.',\n )}\n </p>\n <ul\n className=\"mt-2 list-disc space-y-1 pl-5 text-xs\"\n data-ai-mutation-failed-records\n >\n {failedRecords.map((record) => (\n <li key={record.recordId} data-ai-mutation-failed-record={record.recordId}>\n <span className=\"font-mono\">{record.recordId}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2022</span>\n <span className=\"font-mono\">{record.error.code}</span>\n <span className=\"mx-1 text-muted-foreground\">\u2014</span>\n <span>{record.error.message}</span>\n </li>\n ))}\n </ul>\n </div>\n </Alert>\n )\n }\n\n if (isSuccessStatus(status)) {\n const recordId = result?.recordId ?? action.targetRecordId ?? null\n const href = payload.recordHref ?? null\n return (\n <Alert variant=\"success\" data-ai-mutation-result=\"success\">\n <CheckCircle2 className=\"size-4\" aria-hidden />\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.successTitle', 'Action applied')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {result?.commandName\n ? t(\n 'ai_assistant.chat.mutation_cards.result.successWithCommand',\n 'Completed',\n ) + `: ${result.commandName}`\n : t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The mutation completed successfully.',\n )}\n </p>\n {recordId ? (\n <p className=\"mt-1 text-xs\">\n {href ? (\n <a\n className=\"font-mono text-primary underline\"\n href={href}\n data-ai-mutation-result-link\n >\n {t(\n 'ai_assistant.chat.mutation_cards.result.viewRecord',\n 'View record',\n )}\n : {recordId}\n </a>\n ) : (\n <span className=\"font-mono\" data-ai-mutation-result-record-id>\n {recordId}\n </span>\n )}\n </p>\n ) : null}\n </div>\n </Alert>\n )\n }\n\n // Render the failure alert when the action is in a failure status OR when\n // the dispatcher already captured an `executionResult.error` (the row may\n // not yet have transitioned out of `executing` in the polling snapshot,\n // but the handler error is authoritative \u2014 surface it immediately so the\n // operator never sees a stuck \"applying\u2026\" state silently masking a real\n // failure).\n if (isFailureStatus(status) || result?.error) {\n const code = result?.error?.code ?? status ?? 'failed'\n const message =\n result?.error?.message ??\n t(\n 'ai_assistant.chat.mutation_cards.result.failureBody',\n 'The mutation could not be applied.',\n )\n const errorObj = result?.error\n const errorDetails = errorObj?.details\n const errorInput = errorObj?.input\n const errorName = errorObj?.name\n const onFixWithAi = () => {\n // Build a structured prompt that gives the agent enough context to\n // diagnose and retry without copy/paste from the operator. Keeping\n // it explicit (\"retry with corrected arguments\") nudges the model\n // away from re-issuing the same args (which would just hit the\n // same error). The repository's idempotency check only dedupes\n // active `pending` rows, so a fresh prepareMutation call after a\n // terminal failure always produces a new pending action \u2014 the\n // retry is never silently collapsed.\n //\n // The prompt now embeds the full structured failure context the\n // server captured (Zod issues / fieldErrors, original arguments,\n // failedRecords for batch tools, error name + cause). Without this\n // the operator routinely saw \"Invalid input\" with no field path \u2014\n // the model literally could not fix what it could not see.\n const promptLines: string[] = [\n `The previous call to tool \"${action.toolName}\" failed.`,\n `Error: ${code} \u2014 ${message}.`,\n ]\n if (errorName && errorName !== 'Error') {\n promptLines.push(`Error class: ${errorName}.`)\n }\n if (action.targetEntityType || action.targetRecordId) {\n promptLines.push(\n `Target: ${action.targetEntityType ?? '?'}${action.targetRecordId ? ' / ' + action.targetRecordId : ''}.`,\n )\n }\n\n // Field-level validation issues (Zod, custom). Render as a bulleted\n // list of `path: message` so the model can locate the offender by\n // schema path instead of guessing from a generic message.\n const issues = errorDetails?.issues ?? []\n if (Array.isArray(issues) && issues.length > 0) {\n promptLines.push('', 'Validation issues:')\n for (const issue of issues) {\n const path = Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : '(root)'\n const msg = issue?.message ?? '(no message)'\n const codeHint = issue?.code ? ` [${issue.code}]` : ''\n const expHint =\n issue?.expected || issue?.received\n ? ` (expected ${issue?.expected ?? '?'}, got ${issue?.received ?? '?'})`\n : ''\n promptLines.push(`- ${path}: ${msg}${codeHint}${expHint}`)\n }\n } else if (errorDetails?.fieldErrors && typeof errorDetails.fieldErrors === 'object') {\n const entries = Object.entries(errorDetails.fieldErrors)\n if (entries.length > 0) {\n promptLines.push('', 'Field errors:')\n for (const [path, msgs] of entries) {\n const list = Array.isArray(msgs) ? msgs.join('; ') : String(msgs)\n promptLines.push(`- ${path}: ${list}`)\n }\n }\n }\n\n // Echo the arguments the handler was invoked with so the model can\n // see exactly what it sent and change at least one parameter on\n // retry. JSON-stringified inline for compactness; non-serializable\n // values are dropped on the server side already.\n if (errorInput !== undefined) {\n try {\n const json = JSON.stringify(errorInput, null, 2)\n if (json && json !== '{}' && json.length <= 4000) {\n promptLines.push('', 'Arguments you sent:', '```json', json, '```')\n } else if (json && json.length > 4000) {\n promptLines.push(\n '',\n 'Arguments you sent (truncated):',\n '```json',\n json.slice(0, 4000) + '\\n\u2026 [truncated]',\n '```',\n )\n }\n } catch {\n // ignore\n }\n }\n\n // Surface root-cause when the handler nested another error inside.\n if (errorDetails?.cause !== undefined) {\n try {\n const causeJson = JSON.stringify(errorDetails.cause, null, 2)\n if (causeJson && causeJson !== '{}' && causeJson.length <= 1500) {\n promptLines.push('', 'Underlying cause:', '```json', causeJson, '```')\n }\n } catch {\n // ignore\n }\n }\n\n // Per-record failures from batch tools (Step 5.14). These usually\n // carry the most actionable information for partial-success cases.\n if (failedRecords && failedRecords.length > 0) {\n promptLines.push('', 'Records that failed:')\n for (const rec of failedRecords) {\n promptLines.push(\n `- ${rec.recordId} \u2192 ${rec.error.code}: ${rec.error.message}`,\n )\n }\n }\n\n promptLines.push(\n '',\n 'Diagnose what went wrong using the validation issues / cause / arguments above, correct the arguments, and call the tool again. If the failure indicates missing prerequisites (e.g. a deal needs a linked person/company before commenting), tell me what to fix on the platform side instead of retrying blindly. Do not repeat the exact same arguments \u2014 you must change at least one parameter or stop and explain.',\n )\n dispatchFixRequest({\n message: promptLines.join('\\n'),\n toolName: action.toolName,\n pendingActionId: action.id,\n })\n }\n const visibleIssues = Array.isArray(errorDetails?.issues)\n ? errorDetails!.issues!.filter((entry) => entry && (entry.message || entry.path))\n : []\n return (\n <Alert variant=\"destructive\" data-ai-mutation-result=\"failure\">\n <XCircle className=\"size-4\" aria-hidden />\n <AlertTitle>\n {t(\n 'ai_assistant.chat.mutation_cards.result.failureTitle',\n 'Action failed',\n )}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <div>\n <span className=\"mr-2 font-mono text-xs\" data-ai-mutation-result-code>\n {code}\n </span>\n <span>{message}</span>\n </div>\n {visibleIssues.length > 0 ? (\n <ul\n className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs\"\n data-ai-mutation-result-issues\n >\n {visibleIssues.map((issue, index) => {\n const path =\n Array.isArray(issue?.path) && issue.path.length > 0\n ? issue.path.join('.')\n : null\n return (\n <li key={index}>\n {path ? (\n <span className=\"font-mono\">{path}</span>\n ) : null}\n {path && issue?.message ? <span className=\"mx-1\">\u2014</span> : null}\n {issue?.message ? <span>{issue.message}</span> : null}\n </li>\n )\n })}\n </ul>\n ) : null}\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onFixWithAi}\n data-ai-mutation-result-fix\n >\n <Wand2 className=\"size-4\" aria-hidden />\n <span>\n {t(\n 'ai_assistant.chat.mutation_cards.result.fixWithAi',\n 'Fix with AI',\n )}\n </span>\n </Button>\n </div>\n </div>\n </Alert>\n )\n }\n\n return null\n}\n\nexport default MutationResultCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { MutationPreviewCard } from "./MutationPreviewCard.js";
|
|
3
|
+
import { FieldDiffCard } from "./FieldDiffCard.js";
|
|
4
|
+
import { ConfirmationCard } from "./ConfirmationCard.js";
|
|
5
|
+
import { MutationResultCard } from "./MutationResultCard.js";
|
|
6
|
+
const AI_MUTATION_APPROVAL_CARDS = Object.freeze({
|
|
7
|
+
"mutation-preview-card": MutationPreviewCard,
|
|
8
|
+
"field-diff-card": FieldDiffCard,
|
|
9
|
+
"confirmation-card": ConfirmationCard,
|
|
10
|
+
"mutation-result-card": MutationResultCard
|
|
11
|
+
});
|
|
12
|
+
export {
|
|
13
|
+
AI_MUTATION_APPROVAL_CARDS
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=approval-cards-map.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/ai/parts/approval-cards-map.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport type { AiUiPartComponent, AiUiPartProps } from '../ui-part-registry'\nimport { MutationPreviewCard } from './MutationPreviewCard'\nimport { FieldDiffCard } from './FieldDiffCard'\nimport { ConfirmationCard } from './ConfirmationCard'\nimport { MutationResultCard } from './MutationResultCard'\n\n/**\n * Canonical map of the four Phase 3 mutation-approval cards keyed by their\n * reserved registry component ids. Consumers can spread this into any\n * `AiUiPartRegistry` to wire the live cards at once. Isolated from\n * `parts/index.ts` so the `ui-part-registry` module can import the map\n * without importing the barrel (which would re-export back into the same\n * module and trip Node's \"Cannot access before initialization\" ordering).\n */\nexport const AI_MUTATION_APPROVAL_CARDS: Readonly<\n Record<string, AiUiPartComponent<AiUiPartProps>>\n> = Object.freeze({\n 'mutation-preview-card': MutationPreviewCard as AiUiPartComponent<AiUiPartProps>,\n 'field-diff-card': FieldDiffCard as unknown as AiUiPartComponent<AiUiPartProps>,\n 'confirmation-card': ConfirmationCard as AiUiPartComponent<AiUiPartProps>,\n 'mutation-result-card': MutationResultCard as AiUiPartComponent<AiUiPartProps>,\n})\n"],
|
|
5
|
+
"mappings": ";AAGA,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAU5B,MAAM,6BAET,OAAO,OAAO;AAAA,EAChB,yBAAyB;AAAA,EACzB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,wBAAwB;AAC1B,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { MutationPreviewCard } from "./MutationPreviewCard.js";
|
|
3
|
+
import { FieldDiffCard } from "./FieldDiffCard.js";
|
|
4
|
+
import { ConfirmationCard } from "./ConfirmationCard.js";
|
|
5
|
+
import { MutationResultCard } from "./MutationResultCard.js";
|
|
6
|
+
import {
|
|
7
|
+
useAiPendingActionPolling
|
|
8
|
+
} from "./useAiPendingActionPolling.js";
|
|
9
|
+
import {
|
|
10
|
+
confirmPendingAction,
|
|
11
|
+
cancelPendingAction
|
|
12
|
+
} from "./pending-action-api.js";
|
|
13
|
+
import { AI_MUTATION_APPROVAL_CARDS } from "./approval-cards-map.js";
|
|
14
|
+
export {
|
|
15
|
+
AI_MUTATION_APPROVAL_CARDS,
|
|
16
|
+
ConfirmationCard,
|
|
17
|
+
FieldDiffCard,
|
|
18
|
+
MutationPreviewCard,
|
|
19
|
+
MutationResultCard,
|
|
20
|
+
cancelPendingAction,
|
|
21
|
+
confirmPendingAction,
|
|
22
|
+
useAiPendingActionPolling
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/ai/parts/index.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nexport { MutationPreviewCard } from './MutationPreviewCard'\nexport { FieldDiffCard } from './FieldDiffCard'\nexport { ConfirmationCard } from './ConfirmationCard'\nexport { MutationResultCard } from './MutationResultCard'\nexport {\n useAiPendingActionPolling,\n type UseAiPendingActionPollingOptions,\n type UseAiPendingActionPollingResult,\n} from './useAiPendingActionPolling'\nexport {\n confirmPendingAction,\n cancelPendingAction,\n type PendingActionMutationOk,\n type PendingActionMutationError,\n type PendingActionMutationResult,\n} from './pending-action-api'\nexport type {\n AiPendingActionCardAction,\n AiPendingActionCardStatus,\n AiPendingActionCardFieldDiff,\n AiPendingActionCardRecordDiff,\n AiPendingActionCardFailedRecord,\n AiPendingActionCardExecutionResult,\n} from './types'\nexport { AI_MUTATION_APPROVAL_CARDS } from './approval-cards-map'\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AASP,SAAS,kCAAkC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { apiCall } from "../../backend/utils/apiCall.js";
|
|
3
|
+
const POST_JSON_TIMEOUT_MS = 6e4;
|
|
4
|
+
async function postJson(url, body) {
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const timer = typeof window !== "undefined" ? window.setTimeout(() => controller.abort(), POST_JSON_TIMEOUT_MS) : null;
|
|
7
|
+
try {
|
|
8
|
+
const call = await apiCall(url, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
11
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
12
|
+
signal: controller.signal
|
|
13
|
+
});
|
|
14
|
+
if (call.ok) {
|
|
15
|
+
const data = call.result;
|
|
16
|
+
return { ok: true, data };
|
|
17
|
+
}
|
|
18
|
+
const raw = call.result ?? {};
|
|
19
|
+
const errorMessage = typeof raw.error === "string" && raw.error.length > 0 ? raw.error : `Request failed (${call.status}).`;
|
|
20
|
+
const code = typeof raw.code === "string" ? raw.code : void 0;
|
|
21
|
+
const { error: _err, code: _code, ...extra } = raw;
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: {
|
|
25
|
+
status: call.status,
|
|
26
|
+
code,
|
|
27
|
+
message: errorMessage,
|
|
28
|
+
extra: Object.keys(extra).length > 0 ? extra : void 0
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const aborted = err?.name === "AbortError" || controller.signal.aborted;
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: {
|
|
36
|
+
status: aborted ? 408 : 0,
|
|
37
|
+
code: aborted ? "request_timeout" : "network_error",
|
|
38
|
+
message: aborted ? `Request timed out after ${Math.round(POST_JSON_TIMEOUT_MS / 1e3)}s.` : err instanceof Error ? err.message : "Network error contacting the AI dispatcher."
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
} finally {
|
|
42
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function confirmPendingAction(pendingActionId, options) {
|
|
46
|
+
const base = options?.endpoint ?? "/api/ai_assistant/ai/actions";
|
|
47
|
+
const url = `${base}/${encodeURIComponent(pendingActionId)}/confirm`;
|
|
48
|
+
return postJson(url);
|
|
49
|
+
}
|
|
50
|
+
async function cancelPendingAction(pendingActionId, options) {
|
|
51
|
+
const base = options?.endpoint ?? "/api/ai_assistant/ai/actions";
|
|
52
|
+
const url = `${base}/${encodeURIComponent(pendingActionId)}/cancel`;
|
|
53
|
+
const body = options?.reason ? { reason: options.reason } : void 0;
|
|
54
|
+
return postJson(url, body);
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
cancelPendingAction,
|
|
58
|
+
confirmPendingAction
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=pending-action-api.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/ai/parts/pending-action-api.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { apiCall } from '../../backend/utils/apiCall'\nimport type { AiPendingActionCardAction } from './types'\n\n/**\n * Thin client wrappers over the pending-action confirm/cancel routes\n * (Steps 5.8 / 5.9). Kept here so the mutation-approval cards (Step 5.10)\n * can thread structured error envelopes \u2014 especially the 412 `stale_version`\n * / 412 `schema_drift` / 409 `invalid_status` shapes \u2014 back into the UI\n * without each card reimplementing the same fetch boilerplate.\n *\n * `apiCall` is used (not `apiCallOrThrow`) because the cards need the\n * non-2xx response body (`{ error, code, failedRecords?, issues? }`) to\n * surface a targeted alert, not a generic thrown error.\n */\n\nexport type PendingActionMutationOk = {\n ok: boolean\n pendingAction: AiPendingActionCardAction\n mutationResult?: AiPendingActionCardAction['executionResult']\n}\n\nexport type PendingActionMutationError = {\n status: number\n code?: string\n message: string\n extra?: Record<string, unknown>\n}\n\nexport type PendingActionMutationResult =\n | { ok: true; data: PendingActionMutationOk }\n | { ok: false; error: PendingActionMutationError }\n\n// Hard ceiling for confirm/cancel calls. The dispatcher's mutation gate runs\n// the wrapped tool handler synchronously inside the POST, so for slow\n// providers (LLM-backed handlers, large bulk batches, external APIs) we still\n// give it ~1 minute. Going longer than this is almost always a server-side\n// hang and the operator should see an error rather than an indefinite\n// \"processing\u2026\" spinner.\nconst POST_JSON_TIMEOUT_MS = 60_000\n\nasync function postJson(\n url: string,\n body?: unknown,\n): Promise<PendingActionMutationResult> {\n const controller = new AbortController()\n const timer =\n typeof window !== 'undefined'\n ? window.setTimeout(() => controller.abort(), POST_JSON_TIMEOUT_MS)\n : null\n try {\n const call = await apiCall<Record<string, unknown>>(url, {\n method: 'POST',\n headers: body ? { 'Content-Type': 'application/json' } : undefined,\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n } as RequestInit)\n if (call.ok) {\n const data = call.result as PendingActionMutationOk\n return { ok: true, data }\n }\n const raw = (call.result ?? {}) as {\n error?: unknown\n code?: unknown\n [key: string]: unknown\n }\n const errorMessage =\n typeof raw.error === 'string' && raw.error.length > 0\n ? raw.error\n : `Request failed (${call.status}).`\n const code = typeof raw.code === 'string' ? raw.code : undefined\n const { error: _err, code: _code, ...extra } = raw\n return {\n ok: false,\n error: {\n status: call.status,\n code,\n message: errorMessage,\n extra: Object.keys(extra).length > 0 ? extra : undefined,\n },\n }\n } catch (err) {\n // Aborted (timeout) or network error. Surface a structured envelope so\n // the card can render the failure inline instead of stalling.\n const aborted =\n (err as { name?: string } | null)?.name === 'AbortError' ||\n controller.signal.aborted\n return {\n ok: false,\n error: {\n status: aborted ? 408 : 0,\n code: aborted ? 'request_timeout' : 'network_error',\n message: aborted\n ? `Request timed out after ${Math.round(POST_JSON_TIMEOUT_MS / 1000)}s.`\n : err instanceof Error\n ? err.message\n : 'Network error contacting the AI dispatcher.',\n },\n }\n } finally {\n if (timer !== null) window.clearTimeout(timer)\n }\n}\n\nexport async function confirmPendingAction(\n pendingActionId: string,\n options?: { endpoint?: string },\n): Promise<PendingActionMutationResult> {\n const base = options?.endpoint ?? '/api/ai_assistant/ai/actions'\n const url = `${base}/${encodeURIComponent(pendingActionId)}/confirm`\n return postJson(url)\n}\n\nexport async function cancelPendingAction(\n pendingActionId: string,\n options?: { endpoint?: string; reason?: string },\n): Promise<PendingActionMutationResult> {\n const base = options?.endpoint ?? '/api/ai_assistant/ai/actions'\n const url = `${base}/${encodeURIComponent(pendingActionId)}/cancel`\n const body = options?.reason ? { reason: options.reason } : undefined\n return postJson(url, body)\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,eAAe;AAsCxB,MAAM,uBAAuB;AAE7B,eAAe,SACb,KACA,MACsC;AACtC,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QACJ,OAAO,WAAW,cACd,OAAO,WAAW,MAAM,WAAW,MAAM,GAAG,oBAAoB,IAChE;AACN,MAAI;AACF,UAAM,OAAO,MAAM,QAAiC,KAAK;AAAA,MACvD,QAAQ;AAAA,MACR,SAAS,OAAO,EAAE,gBAAgB,mBAAmB,IAAI;AAAA,MACzD,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AAAA,MACpC,QAAQ,WAAW;AAAA,IACrB,CAAgB;AAChB,QAAI,KAAK,IAAI;AACX,YAAM,OAAO,KAAK;AAClB,aAAO,EAAE,IAAI,MAAM,KAAK;AAAA,IAC1B;AACA,UAAM,MAAO,KAAK,UAAU,CAAC;AAK7B,UAAM,eACJ,OAAO,IAAI,UAAU,YAAY,IAAI,MAAM,SAAS,IAChD,IAAI,QACJ,mBAAmB,KAAK,MAAM;AACpC,UAAM,OAAO,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AACvD,UAAM,EAAE,OAAO,MAAM,MAAM,OAAO,GAAG,MAAM,IAAI;AAC/C,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,QAAQ,KAAK;AAAA,QACb;AAAA,QACA,SAAS;AAAA,QACT,OAAO,OAAO,KAAK,KAAK,EAAE,SAAS,IAAI,QAAQ;AAAA,MACjD;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AAGZ,UAAM,UACH,KAAkC,SAAS,gBAC5C,WAAW,OAAO;AACpB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,QAAQ,UAAU,MAAM;AAAA,QACxB,MAAM,UAAU,oBAAoB;AAAA,QACpC,SAAS,UACL,2BAA2B,KAAK,MAAM,uBAAuB,GAAI,CAAC,OAClE,eAAe,QACb,IAAI,UACJ;AAAA,MACR;AAAA,IACF;AAAA,EACF,UAAE;AACA,QAAI,UAAU,KAAM,QAAO,aAAa,KAAK;AAAA,EAC/C;AACF;AAEA,eAAsB,qBACpB,iBACA,SACsC;AACtC,QAAM,OAAO,SAAS,YAAY;AAClC,QAAM,MAAM,GAAG,IAAI,IAAI,mBAAmB,eAAe,CAAC;AAC1D,SAAO,SAAS,GAAG;AACrB;AAEA,eAAsB,oBACpB,iBACA,SACsC;AACtC,QAAM,OAAO,SAAS,YAAY;AAClC,QAAM,MAAM,GAAG,IAAI,IAAI,mBAAmB,eAAe,CAAC;AAC1D,QAAM,OAAO,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI;AAC5D,SAAO,SAAS,KAAK,IAAI;AAC3B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { apiCallOrThrow } from "../../backend/utils/apiCall.js";
|
|
4
|
+
const TERMINAL_STATUSES = [
|
|
5
|
+
"confirmed",
|
|
6
|
+
"cancelled",
|
|
7
|
+
"failed",
|
|
8
|
+
"expired"
|
|
9
|
+
];
|
|
10
|
+
function isTerminal(status) {
|
|
11
|
+
if (!status) return false;
|
|
12
|
+
return TERMINAL_STATUSES.includes(status);
|
|
13
|
+
}
|
|
14
|
+
async function fetchPendingAction(pendingActionId, endpoint) {
|
|
15
|
+
const url = `${endpoint}/${encodeURIComponent(pendingActionId)}`;
|
|
16
|
+
const call = await apiCallOrThrow(url, { method: "GET" });
|
|
17
|
+
const body = call.result;
|
|
18
|
+
if (body && typeof body === "object") {
|
|
19
|
+
if (body.pendingAction) {
|
|
20
|
+
return { pendingAction: body.pendingAction, error: null };
|
|
21
|
+
}
|
|
22
|
+
if (typeof body.id === "string" && typeof body.status === "string") {
|
|
23
|
+
return { pendingAction: body, error: null };
|
|
24
|
+
}
|
|
25
|
+
if (body.error) {
|
|
26
|
+
return {
|
|
27
|
+
pendingAction: null,
|
|
28
|
+
error: { message: body.error, code: body.code }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { pendingAction: null, error: null };
|
|
33
|
+
}
|
|
34
|
+
function useAiPendingActionPolling(options) {
|
|
35
|
+
const {
|
|
36
|
+
pendingActionId,
|
|
37
|
+
intervalMs = 3e3,
|
|
38
|
+
endpoint = "/api/ai_assistant/ai/actions",
|
|
39
|
+
disabled = false
|
|
40
|
+
} = options;
|
|
41
|
+
const [action, setAction] = React.useState(null);
|
|
42
|
+
const [error, setError] = React.useState(null);
|
|
43
|
+
const [isPolling, setIsPolling] = React.useState(!disabled);
|
|
44
|
+
const mountedRef = React.useRef(true);
|
|
45
|
+
const timerRef = React.useRef(null);
|
|
46
|
+
const statusRef = React.useRef(null);
|
|
47
|
+
const clearTimer = React.useCallback(() => {
|
|
48
|
+
if (timerRef.current) {
|
|
49
|
+
clearTimeout(timerRef.current);
|
|
50
|
+
timerRef.current = null;
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
const refresh = React.useCallback(async () => {
|
|
54
|
+
if (!pendingActionId) return null;
|
|
55
|
+
try {
|
|
56
|
+
const result = await fetchPendingAction(pendingActionId, endpoint);
|
|
57
|
+
if (!mountedRef.current) return result.pendingAction ?? null;
|
|
58
|
+
if (result.error) {
|
|
59
|
+
setError(result.error);
|
|
60
|
+
} else {
|
|
61
|
+
setError(null);
|
|
62
|
+
}
|
|
63
|
+
if (result.pendingAction) {
|
|
64
|
+
setAction(result.pendingAction);
|
|
65
|
+
statusRef.current = result.pendingAction.status;
|
|
66
|
+
}
|
|
67
|
+
return result.pendingAction;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (!mountedRef.current) return null;
|
|
70
|
+
const message = err instanceof Error ? err.message : "Failed to load pending action.";
|
|
71
|
+
setError({ message });
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}, [endpoint, pendingActionId]);
|
|
75
|
+
const scheduleNext = React.useCallback(() => {
|
|
76
|
+
clearTimer();
|
|
77
|
+
if (!mountedRef.current) return;
|
|
78
|
+
if (disabled) {
|
|
79
|
+
setIsPolling(false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (isTerminal(statusRef.current)) {
|
|
83
|
+
setIsPolling(false);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
setIsPolling(true);
|
|
87
|
+
timerRef.current = setTimeout(async () => {
|
|
88
|
+
await refresh();
|
|
89
|
+
scheduleNext();
|
|
90
|
+
}, intervalMs);
|
|
91
|
+
}, [clearTimer, disabled, intervalMs, refresh]);
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
mountedRef.current = true;
|
|
94
|
+
statusRef.current = null;
|
|
95
|
+
if (disabled) {
|
|
96
|
+
setIsPolling(false);
|
|
97
|
+
return () => {
|
|
98
|
+
mountedRef.current = false;
|
|
99
|
+
clearTimer();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
setIsPolling(true);
|
|
103
|
+
void refresh().then(() => {
|
|
104
|
+
if (!mountedRef.current) return;
|
|
105
|
+
scheduleNext();
|
|
106
|
+
});
|
|
107
|
+
return () => {
|
|
108
|
+
mountedRef.current = false;
|
|
109
|
+
clearTimer();
|
|
110
|
+
};
|
|
111
|
+
}, [pendingActionId, endpoint, intervalMs, disabled]);
|
|
112
|
+
const status = action?.status ?? null;
|
|
113
|
+
return {
|
|
114
|
+
action,
|
|
115
|
+
status,
|
|
116
|
+
isPolling: isPolling && !isTerminal(status),
|
|
117
|
+
error,
|
|
118
|
+
refresh
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
var useAiPendingActionPolling_default = useAiPendingActionPolling;
|
|
122
|
+
export {
|
|
123
|
+
useAiPendingActionPolling_default as default,
|
|
124
|
+
useAiPendingActionPolling
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=useAiPendingActionPolling.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/ai/parts/useAiPendingActionPolling.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow } from '../../backend/utils/apiCall'\nimport type { AiPendingActionCardAction, AiPendingActionCardStatus } from './types'\n\n/**\n * Shared polling hook for the Phase 3 mutation-approval cards (Step 5.10).\n *\n * Responsibilities:\n * - Fetch `GET /api/ai_assistant/ai/actions/:id` on mount, even when the\n * server previously streamed a preview card. This is the \"reconnect\" path:\n * after a page reload or navigation away+back, the card recovers the\n * current pending-action state instead of staying blank.\n * - Poll every 3 seconds while the status is non-terminal\n * (`pending` / `executing`). Terminal states (`confirmed`, `cancelled`,\n * `failed`, `expired`) stop polling.\n * - Expose a `refresh()` force-fetch helper the confirmation card uses after\n * a confirm POST races with the polling loop.\n *\n * The hook owns a single `setInterval`/`setTimeout` \u2014 unmounting clears\n * every outstanding timer. This is required for the Jest fake-timers\n * \"mount, unmount mid-poll\" test contract.\n */\n\nconst TERMINAL_STATUSES: ReadonlyArray<AiPendingActionCardStatus> = [\n 'confirmed',\n 'cancelled',\n 'failed',\n 'expired',\n]\n\nfunction isTerminal(status: AiPendingActionCardStatus | null): boolean {\n if (!status) return false\n return TERMINAL_STATUSES.includes(status)\n}\n\nexport interface UseAiPendingActionPollingOptions {\n pendingActionId: string\n /**\n * Poll interval in ms while the status is non-terminal. Defaults to 3000.\n */\n intervalMs?: number\n /**\n * Endpoint base. Override to point at a mock during tests.\n */\n endpoint?: string\n /**\n * When true, the hook does NOT schedule any network activity. Used by the\n * result card which already holds a terminal state and only needs to read\n * what the preview card fetched.\n */\n disabled?: boolean\n}\n\nexport interface AiPendingActionFetchResult {\n pendingAction: AiPendingActionCardAction | null\n error?: { code?: string; message: string } | null\n}\n\nexport interface UseAiPendingActionPollingResult {\n action: AiPendingActionCardAction | null\n status: AiPendingActionCardStatus | null\n isPolling: boolean\n error: { code?: string; message: string } | null\n refresh: () => Promise<AiPendingActionCardAction | null>\n}\n\nasync function fetchPendingAction(\n pendingActionId: string,\n endpoint: string,\n): Promise<AiPendingActionFetchResult> {\n const url = `${endpoint}/${encodeURIComponent(pendingActionId)}`\n const call = await apiCallOrThrow<unknown>(url, { method: 'GET' })\n const body = call.result as\n | (Partial<AiPendingActionCardAction> & {\n pendingAction?: AiPendingActionCardAction\n error?: string\n code?: string\n })\n | null\n | undefined\n\n // The GET route returns the bare action object (`serializePendingActionForClient(row)`),\n // but earlier client code expected it under a `pendingAction` envelope.\n // Accept BOTH shapes so the cards work whether the dispatcher wraps or\n // not \u2014 bare-object payloads were the actual cause of empty\n // `fieldDiff` and missing `executionResult.error` displays in\n // MutationPreviewCard / ConfirmationCard / MutationResultCard.\n if (body && typeof body === 'object') {\n if (body.pendingAction) {\n return { pendingAction: body.pendingAction, error: null }\n }\n if (\n typeof (body as Partial<AiPendingActionCardAction>).id === 'string' &&\n typeof (body as Partial<AiPendingActionCardAction>).status === 'string'\n ) {\n return { pendingAction: body as AiPendingActionCardAction, error: null }\n }\n if (body.error) {\n return {\n pendingAction: null,\n error: { message: body.error, code: body.code },\n }\n }\n }\n return { pendingAction: null, error: null }\n}\n\nexport function useAiPendingActionPolling(\n options: UseAiPendingActionPollingOptions,\n): UseAiPendingActionPollingResult {\n const {\n pendingActionId,\n intervalMs = 3000,\n endpoint = '/api/ai_assistant/ai/actions',\n disabled = false,\n } = options\n\n const [action, setAction] = React.useState<AiPendingActionCardAction | null>(null)\n const [error, setError] = React.useState<{ code?: string; message: string } | null>(null)\n const [isPolling, setIsPolling] = React.useState<boolean>(!disabled)\n\n const mountedRef = React.useRef(true)\n const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n const statusRef = React.useRef<AiPendingActionCardStatus | null>(null)\n\n const clearTimer = React.useCallback(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current)\n timerRef.current = null\n }\n }, [])\n\n const refresh = React.useCallback(async (): Promise<AiPendingActionCardAction | null> => {\n if (!pendingActionId) return null\n try {\n const result = await fetchPendingAction(pendingActionId, endpoint)\n if (!mountedRef.current) return result.pendingAction ?? null\n if (result.error) {\n setError(result.error)\n } else {\n setError(null)\n }\n if (result.pendingAction) {\n setAction(result.pendingAction)\n statusRef.current = result.pendingAction.status\n }\n return result.pendingAction\n } catch (err) {\n if (!mountedRef.current) return null\n const message = err instanceof Error ? err.message : 'Failed to load pending action.'\n setError({ message })\n return null\n }\n }, [endpoint, pendingActionId])\n\n const scheduleNext = React.useCallback(() => {\n clearTimer()\n if (!mountedRef.current) return\n if (disabled) {\n setIsPolling(false)\n return\n }\n if (isTerminal(statusRef.current)) {\n setIsPolling(false)\n return\n }\n setIsPolling(true)\n timerRef.current = setTimeout(async () => {\n await refresh()\n scheduleNext()\n }, intervalMs)\n }, [clearTimer, disabled, intervalMs, refresh])\n\n React.useEffect(() => {\n mountedRef.current = true\n statusRef.current = null\n if (disabled) {\n setIsPolling(false)\n return () => {\n mountedRef.current = false\n clearTimer()\n }\n }\n setIsPolling(true)\n // Always fetch on mount \u2014 the \"reconnect behavior\" guarantee.\n void refresh().then(() => {\n if (!mountedRef.current) return\n scheduleNext()\n })\n return () => {\n mountedRef.current = false\n clearTimer()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pendingActionId, endpoint, intervalMs, disabled])\n\n const status = action?.status ?? null\n\n return {\n action,\n status,\n isPolling: isPolling && !isTerminal(status),\n error,\n refresh,\n }\n}\n\nexport default useAiPendingActionPolling\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|