@open-mercato/ui 0.9.1-develop.7519.1.38f51f8dbb → 0.9.1-develop.7536.1.f9a5776a07
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.
|
@@ -14,7 +14,10 @@ function isSuccessStatus(status) {
|
|
|
14
14
|
return status === "confirmed";
|
|
15
15
|
}
|
|
16
16
|
function isFailureStatus(status) {
|
|
17
|
-
return status === "failed" || status === "
|
|
17
|
+
return status === "failed" || status === "expired";
|
|
18
|
+
}
|
|
19
|
+
function isCancelledStatus(status) {
|
|
20
|
+
return status === "cancelled";
|
|
18
21
|
}
|
|
19
22
|
function MutationResultCard(props) {
|
|
20
23
|
const t = useT();
|
|
@@ -86,6 +89,16 @@ function MutationResultCard(props) {
|
|
|
86
89
|
] })
|
|
87
90
|
] });
|
|
88
91
|
}
|
|
92
|
+
if (isCancelledStatus(status)) {
|
|
93
|
+
const reason = result?.error?.message?.trim() || null;
|
|
94
|
+
return /* @__PURE__ */ jsxs(Alert, { status: "information", "data-ai-mutation-result": "cancelled", children: [
|
|
95
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.chat.mutation_cards.result.cancelledTitle", "Proposal cancelled") }),
|
|
96
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm leading-relaxed", children: /* @__PURE__ */ jsx("p", { children: reason ?? t(
|
|
97
|
+
"ai_assistant.chat.mutation_cards.result.cancelledBody",
|
|
98
|
+
"You cancelled this proposed change. No changes were made."
|
|
99
|
+
) }) })
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
89
102
|
if (isFailureStatus(status) || result?.error) {
|
|
90
103
|
const code = result?.error?.code ?? status ?? "failed";
|
|
91
104
|
const message = result?.error?.message ?? t(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/ai/parts/MutationResultCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Wand2 } 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 status=\"warning\" data-ai-mutation-result=\"partial\">\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 status=\"success\" data-ai-mutation-result=\"success\">\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 {t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The requested change was saved successfully.',\n )}\n </p>\n {recordId && href ? (\n <p className=\"mt-1 text-xs\">\n <a\n className=\"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 </a>\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 status=\"error\" data-ai-mutation-result=\"failure\">\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": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Wand2 } 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 === 'expired'\n}\n\nfunction isCancelledStatus(status: AiPendingActionCardStatus | null): boolean {\n return status === 'cancelled'\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 status=\"warning\" data-ai-mutation-result=\"partial\">\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 status=\"success\" data-ai-mutation-result=\"success\">\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 {t(\n 'ai_assistant.chat.mutation_cards.result.successBody',\n 'The requested change was saved successfully.',\n )}\n </p>\n {recordId && href ? (\n <p className=\"mt-1 text-xs\">\n <a\n className=\"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 </a>\n </p>\n ) : null}\n </div>\n </Alert>\n )\n }\n\n // A deliberate cancellation is not a failure: no destructive styling, no\n // internal error code, and no \"Fix with AI\" (which would just re-propose\n // the change the operator just rejected). Show the operator's own\n // cancellation reason verbatim when they gave one, otherwise a neutral\n // localized default.\n if (isCancelledStatus(status)) {\n const reason = result?.error?.message?.trim() || null\n return (\n <Alert status=\"information\" data-ai-mutation-result=\"cancelled\">\n <AlertTitle>\n {t('ai_assistant.chat.mutation_cards.result.cancelledTitle', 'Proposal cancelled')}\n </AlertTitle>\n <div className=\"text-sm leading-relaxed\">\n <p>\n {reason ??\n t(\n 'ai_assistant.chat.mutation_cards.result.cancelledBody',\n 'You cancelled this proposed change. No changes were made.',\n )}\n </p>\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 status=\"error\" data-ai-mutation-result=\"failure\">\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
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.9.1-develop.
|
|
3
|
+
"version": "0.9.1-develop.7536.1.f9a5776a07",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.9.1-develop.7536.1.f9a5776a07",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.9.1-develop.7536.1.f9a5776a07",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
|
@@ -54,7 +54,11 @@ function isSuccessStatus(status: AiPendingActionCardStatus | null): boolean {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function isFailureStatus(status: AiPendingActionCardStatus | null): boolean {
|
|
57
|
-
return status === 'failed' || status === '
|
|
57
|
+
return status === 'failed' || status === 'expired'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isCancelledStatus(status: AiPendingActionCardStatus | null): boolean {
|
|
61
|
+
return status === 'cancelled'
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export function MutationResultCard(props: MutationResultCardProps) {
|
|
@@ -146,6 +150,31 @@ export function MutationResultCard(props: MutationResultCardProps) {
|
|
|
146
150
|
)
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
// A deliberate cancellation is not a failure: no destructive styling, no
|
|
154
|
+
// internal error code, and no "Fix with AI" (which would just re-propose
|
|
155
|
+
// the change the operator just rejected). Show the operator's own
|
|
156
|
+
// cancellation reason verbatim when they gave one, otherwise a neutral
|
|
157
|
+
// localized default.
|
|
158
|
+
if (isCancelledStatus(status)) {
|
|
159
|
+
const reason = result?.error?.message?.trim() || null
|
|
160
|
+
return (
|
|
161
|
+
<Alert status="information" data-ai-mutation-result="cancelled">
|
|
162
|
+
<AlertTitle>
|
|
163
|
+
{t('ai_assistant.chat.mutation_cards.result.cancelledTitle', 'Proposal cancelled')}
|
|
164
|
+
</AlertTitle>
|
|
165
|
+
<div className="text-sm leading-relaxed">
|
|
166
|
+
<p>
|
|
167
|
+
{reason ??
|
|
168
|
+
t(
|
|
169
|
+
'ai_assistant.chat.mutation_cards.result.cancelledBody',
|
|
170
|
+
'You cancelled this proposed change. No changes were made.',
|
|
171
|
+
)}
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
</Alert>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
149
178
|
// Render the failure alert when the action is in a failure status OR when
|
|
150
179
|
// the dispatcher already captured an `executionResult.error` (the row may
|
|
151
180
|
// not yet have transitioned out of `executing` in the polling snapshot,
|
|
@@ -23,6 +23,8 @@ const dict = {
|
|
|
23
23
|
'ai_assistant.chat.mutation_cards.result.partialBody': 'Some records could not be updated.',
|
|
24
24
|
'ai_assistant.chat.mutation_cards.result.failureTitle': 'Action failed',
|
|
25
25
|
'ai_assistant.chat.mutation_cards.result.failureBody': 'The mutation could not be applied.',
|
|
26
|
+
'ai_assistant.chat.mutation_cards.result.cancelledTitle': 'Proposal cancelled',
|
|
27
|
+
'ai_assistant.chat.mutation_cards.result.cancelledBody': 'You cancelled this proposed change. No changes were made.',
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function baseAction(
|
|
@@ -144,4 +146,50 @@ describe('MutationResultCard', () => {
|
|
|
144
146
|
const code = document.querySelector('[data-ai-mutation-result-code]')
|
|
145
147
|
expect(code?.textContent).toBe('tool_execution_failed')
|
|
146
148
|
})
|
|
149
|
+
|
|
150
|
+
it('renders a neutral cancelled variant without an error code or Fix with AI button', () => {
|
|
151
|
+
installPollingMock(
|
|
152
|
+
baseAction({
|
|
153
|
+
status: 'cancelled',
|
|
154
|
+
executionResult: {
|
|
155
|
+
error: { code: 'cancelled_by_user', message: '' },
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
renderWithProviders(
|
|
160
|
+
<MutationResultCard componentId="mutation-result-card" pendingActionId="pa-1" />,
|
|
161
|
+
{ dict },
|
|
162
|
+
)
|
|
163
|
+
expect(
|
|
164
|
+
document.querySelector('[data-ai-mutation-result="cancelled"]'),
|
|
165
|
+
).not.toBeNull()
|
|
166
|
+
expect(document.querySelector('[data-ai-mutation-result="failure"]')).toBeNull()
|
|
167
|
+
expect(screen.getByText('Proposal cancelled')).toBeInTheDocument()
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByText('You cancelled this proposed change. No changes were made.'),
|
|
170
|
+
).toBeInTheDocument()
|
|
171
|
+
expect(document.querySelector('[data-ai-mutation-result-code]')).toBeNull()
|
|
172
|
+
expect(document.querySelector('[data-ai-mutation-result-fix]')).toBeNull()
|
|
173
|
+
expect(screen.queryByText('cancelled_by_user')).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('shows the operator-supplied cancellation reason on the cancelled variant', () => {
|
|
177
|
+
installPollingMock(
|
|
178
|
+
baseAction({
|
|
179
|
+
status: 'cancelled',
|
|
180
|
+
executionResult: {
|
|
181
|
+
error: { code: 'cancelled_by_user', message: 'Customer changed their mind' },
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
renderWithProviders(
|
|
186
|
+
<MutationResultCard componentId="mutation-result-card" pendingActionId="pa-1" />,
|
|
187
|
+
{ dict },
|
|
188
|
+
)
|
|
189
|
+
expect(screen.getByText('Customer changed their mind')).toBeInTheDocument()
|
|
190
|
+
expect(
|
|
191
|
+
screen.queryByText('You cancelled this proposed change. No changes were made.'),
|
|
192
|
+
).toBeNull()
|
|
193
|
+
expect(document.querySelector('[data-ai-mutation-result-fix]')).toBeNull()
|
|
194
|
+
})
|
|
147
195
|
})
|