@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,173 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Info } from 'lucide-react'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { Alert, AlertDescription } from '../../primitives/alert'
|
|
7
|
+
import type {
|
|
8
|
+
AiPendingActionCardFieldDiff,
|
|
9
|
+
AiPendingActionCardRecordDiff,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Presentational card rendering a `fieldDiff` list in compact three-column
|
|
14
|
+
* form (field | before | after) with DS-compliant semantic-token colors.
|
|
15
|
+
* Accepts either a flat `fieldDiff[]` (single-record preview) or grouped
|
|
16
|
+
* `records[]` (batch preview) — when both are supplied, `records` wins,
|
|
17
|
+
* matching the server-side `AiPendingAction` contract (spec §8 rule 2).
|
|
18
|
+
*/
|
|
19
|
+
export interface FieldDiffCardProps {
|
|
20
|
+
fieldDiff?: AiPendingActionCardFieldDiff[] | null
|
|
21
|
+
records?: AiPendingActionCardRecordDiff[] | null
|
|
22
|
+
/** Optional forwarded componentId for the registry renderer. */
|
|
23
|
+
componentId?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatValue(value: unknown): string {
|
|
27
|
+
if (value == null) return ''
|
|
28
|
+
if (typeof value === 'string') return value
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(value)
|
|
31
|
+
} catch {
|
|
32
|
+
return String(value)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function DiffRow({ entry }: { entry: AiPendingActionCardFieldDiff }) {
|
|
37
|
+
const before = formatValue(entry.before)
|
|
38
|
+
const after = formatValue(entry.after)
|
|
39
|
+
return (
|
|
40
|
+
<tr className="border-b border-border last:border-b-0" data-ai-field-diff-row>
|
|
41
|
+
<td className="py-1.5 pr-4 text-xs font-mono text-muted-foreground align-top">
|
|
42
|
+
{entry.field}
|
|
43
|
+
</td>
|
|
44
|
+
<td
|
|
45
|
+
className="py-1.5 pr-4 text-sm align-top text-status-warning-text"
|
|
46
|
+
data-ai-field-diff-before
|
|
47
|
+
>
|
|
48
|
+
<span className="line-through break-all">{before || '—'}</span>
|
|
49
|
+
</td>
|
|
50
|
+
<td
|
|
51
|
+
className="py-1.5 text-sm align-top text-status-success-text"
|
|
52
|
+
data-ai-field-diff-after
|
|
53
|
+
>
|
|
54
|
+
<span className="font-medium break-all">{after || '—'}</span>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function DiffTable({ rows, fieldHeader, beforeHeader, afterHeader }: {
|
|
61
|
+
rows: AiPendingActionCardFieldDiff[]
|
|
62
|
+
fieldHeader: string
|
|
63
|
+
beforeHeader: string
|
|
64
|
+
afterHeader: string
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<table className="w-full" data-ai-field-diff-table>
|
|
68
|
+
<thead>
|
|
69
|
+
<tr className="border-b border-border">
|
|
70
|
+
<th className="py-1 pr-4 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
71
|
+
{fieldHeader}
|
|
72
|
+
</th>
|
|
73
|
+
<th className="py-1 pr-4 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
74
|
+
{beforeHeader}
|
|
75
|
+
</th>
|
|
76
|
+
<th className="py-1 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
77
|
+
{afterHeader}
|
|
78
|
+
</th>
|
|
79
|
+
</tr>
|
|
80
|
+
</thead>
|
|
81
|
+
<tbody>
|
|
82
|
+
{rows.map((entry, idx) => (
|
|
83
|
+
<DiffRow key={`${entry.field}-${idx}`} entry={entry} />
|
|
84
|
+
))}
|
|
85
|
+
</tbody>
|
|
86
|
+
</table>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function FieldDiffCard({ fieldDiff, records }: FieldDiffCardProps) {
|
|
91
|
+
const t = useT()
|
|
92
|
+
const fieldHeader = t(
|
|
93
|
+
'ai_assistant.chat.mutation_cards.diff.fieldHeader',
|
|
94
|
+
'Field',
|
|
95
|
+
)
|
|
96
|
+
const beforeHeader = t(
|
|
97
|
+
'ai_assistant.chat.mutation_cards.diff.beforeHeader',
|
|
98
|
+
'Before',
|
|
99
|
+
)
|
|
100
|
+
const afterHeader = t(
|
|
101
|
+
'ai_assistant.chat.mutation_cards.diff.afterHeader',
|
|
102
|
+
'After',
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const batch = Array.isArray(records) && records.length > 0 ? records : null
|
|
106
|
+
const flat = Array.isArray(fieldDiff) ? fieldDiff : []
|
|
107
|
+
|
|
108
|
+
if (batch) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex flex-col gap-3" data-ai-field-diff-mode="batch">
|
|
111
|
+
{batch.map((record) => (
|
|
112
|
+
<section
|
|
113
|
+
key={record.recordId}
|
|
114
|
+
className="rounded-md border border-border bg-background p-3"
|
|
115
|
+
data-ai-field-diff-record={record.recordId}
|
|
116
|
+
>
|
|
117
|
+
<header className="mb-2 flex items-baseline justify-between gap-2">
|
|
118
|
+
<h4 className="text-sm font-semibold">{record.label}</h4>
|
|
119
|
+
<span className="text-xs font-mono text-muted-foreground">
|
|
120
|
+
{record.entityType}
|
|
121
|
+
</span>
|
|
122
|
+
</header>
|
|
123
|
+
{record.fieldDiff.length > 0 ? (
|
|
124
|
+
<DiffTable
|
|
125
|
+
rows={record.fieldDiff}
|
|
126
|
+
fieldHeader={fieldHeader}
|
|
127
|
+
beforeHeader={beforeHeader}
|
|
128
|
+
afterHeader={afterHeader}
|
|
129
|
+
/>
|
|
130
|
+
) : (
|
|
131
|
+
<Alert variant="info">
|
|
132
|
+
<Info className="size-4" aria-hidden />
|
|
133
|
+
<AlertDescription>
|
|
134
|
+
{t(
|
|
135
|
+
'ai_assistant.chat.mutation_cards.diff.empty',
|
|
136
|
+
'No field changes for this record.',
|
|
137
|
+
)}
|
|
138
|
+
</AlertDescription>
|
|
139
|
+
</Alert>
|
|
140
|
+
)}
|
|
141
|
+
</section>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (flat.length === 0) {
|
|
148
|
+
return (
|
|
149
|
+
<Alert variant="info" data-ai-field-diff-mode="empty">
|
|
150
|
+
<Info className="size-4" aria-hidden />
|
|
151
|
+
<AlertDescription>
|
|
152
|
+
{t(
|
|
153
|
+
'ai_assistant.chat.mutation_cards.diff.empty',
|
|
154
|
+
'No field changes for this record.',
|
|
155
|
+
)}
|
|
156
|
+
</AlertDescription>
|
|
157
|
+
</Alert>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div data-ai-field-diff-mode="flat">
|
|
163
|
+
<DiffTable
|
|
164
|
+
rows={flat}
|
|
165
|
+
fieldHeader={fieldHeader}
|
|
166
|
+
beforeHeader={beforeHeader}
|
|
167
|
+
afterHeader={afterHeader}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default FieldDiffCard
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { ChevronDown, Eye, ShieldAlert } from 'lucide-react'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { Button } from '../../primitives/button'
|
|
7
|
+
import { useAiShortcuts } from '../useAiShortcuts'
|
|
8
|
+
import type { AiUiPartProps } from '../ui-part-registry'
|
|
9
|
+
import { confirmPendingAction, cancelPendingAction } from './pending-action-api'
|
|
10
|
+
import { useAiPendingActionPolling } from './useAiPendingActionPolling'
|
|
11
|
+
import { FieldDiffCard } from './FieldDiffCard'
|
|
12
|
+
import { ConfirmationCard } from './ConfirmationCard'
|
|
13
|
+
import { MutationResultCard } from './MutationResultCard'
|
|
14
|
+
import type { AiPendingActionCardAction } from './types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mutation-approval preview card. Rendered by the server-emitted
|
|
18
|
+
* `mutation-preview-card` UI part (spec §9.2).
|
|
19
|
+
*
|
|
20
|
+
* Responsibilities:
|
|
21
|
+
* - Fetch the current pending-action row via the shared polling hook so the
|
|
22
|
+
* card recovers state on page reload (reconnect behavior).
|
|
23
|
+
* - Render the top-level `fieldDiff` OR a per-record `records[]` summary
|
|
24
|
+
* with a drill-in link.
|
|
25
|
+
* - Provide `Confirm` / `Cancel` / `Review Details` actions with shared
|
|
26
|
+
* keyboard shortcuts (`Cmd/Ctrl+Enter` → confirm, `Escape` → cancel).
|
|
27
|
+
* - Flip to the {@link ConfirmationCard} once the user confirms, and
|
|
28
|
+
* further to the {@link MutationResultCard} once the row becomes
|
|
29
|
+
* terminal.
|
|
30
|
+
*/
|
|
31
|
+
export interface MutationPreviewCardPayload {
|
|
32
|
+
/** Optional server-serialized pending action snapshot for the initial render. */
|
|
33
|
+
pendingAction?: AiPendingActionCardAction
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MutationPreviewCardProps extends AiUiPartProps {
|
|
37
|
+
/** Optional injected action for tests — bypasses the polling fetch. */
|
|
38
|
+
initialAction?: AiPendingActionCardAction
|
|
39
|
+
/** Endpoint base override for tests. */
|
|
40
|
+
endpoint?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function summarizeBatch(
|
|
44
|
+
records: NonNullable<AiPendingActionCardAction['records']>,
|
|
45
|
+
): { count: number; labels: string[] } {
|
|
46
|
+
const labels = records.slice(0, 3).map((record) => record.label)
|
|
47
|
+
return { count: records.length, labels }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function MutationPreviewCard(props: MutationPreviewCardProps) {
|
|
51
|
+
const t = useT()
|
|
52
|
+
const pendingActionId = props.pendingActionId ?? ''
|
|
53
|
+
const payload = (props.payload as MutationPreviewCardPayload | undefined) ?? {}
|
|
54
|
+
const injected = props.initialAction ?? payload.pendingAction ?? null
|
|
55
|
+
|
|
56
|
+
const { action: polled, refresh } = useAiPendingActionPolling({
|
|
57
|
+
pendingActionId,
|
|
58
|
+
endpoint: props.endpoint,
|
|
59
|
+
disabled: !pendingActionId,
|
|
60
|
+
})
|
|
61
|
+
const action = polled ?? injected
|
|
62
|
+
|
|
63
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
64
|
+
const [phase, setPhase] = React.useState<'preview' | 'confirming'>('preview')
|
|
65
|
+
const [confirmError, setConfirmError] = React.useState<{
|
|
66
|
+
status: number
|
|
67
|
+
code?: string
|
|
68
|
+
message: string
|
|
69
|
+
extra?: Record<string, unknown>
|
|
70
|
+
} | null>(null)
|
|
71
|
+
|
|
72
|
+
const handleConfirm = React.useCallback(async () => {
|
|
73
|
+
if (!pendingActionId) return
|
|
74
|
+
if (phase !== 'preview') return
|
|
75
|
+
setPhase('confirming')
|
|
76
|
+
setConfirmError(null)
|
|
77
|
+
const result = await confirmPendingAction(pendingActionId, {
|
|
78
|
+
endpoint: props.endpoint,
|
|
79
|
+
})
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
// Network / timeout / 4xx / 5xx — surface the envelope and rewind the
|
|
82
|
+
// card to the preview phase so the operator can read the error,
|
|
83
|
+
// edit the proposal upstream, or retry.
|
|
84
|
+
setConfirmError(result.error)
|
|
85
|
+
setPhase('preview')
|
|
86
|
+
await refresh()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
// HTTP 200 path. The dispatcher returns `ok: false` AND a populated
|
|
90
|
+
// `mutationResult.error` when the wrapped tool handler failed inside
|
|
91
|
+
// the confirm route — the row is already in a terminal state but the
|
|
92
|
+
// overall HTTP call succeeded. Treat that as a confirm error too so
|
|
93
|
+
// the alert renders inline instead of leaving the card on the
|
|
94
|
+
// generic "applying…" spinner forever.
|
|
95
|
+
const handlerError = result.data?.mutationResult?.error
|
|
96
|
+
if (result.data?.ok === false || handlerError) {
|
|
97
|
+
const mappedCode =
|
|
98
|
+
typeof handlerError?.code === 'string' && handlerError.code.length > 0
|
|
99
|
+
? handlerError.code
|
|
100
|
+
: 'execution_failed'
|
|
101
|
+
setConfirmError({
|
|
102
|
+
status: 200,
|
|
103
|
+
code: mappedCode,
|
|
104
|
+
message:
|
|
105
|
+
handlerError?.message ??
|
|
106
|
+
t(
|
|
107
|
+
'ai_assistant.chat.mutation_cards.preview.handlerError',
|
|
108
|
+
'The mutation handler reported an error. Review the details and re-propose if needed.',
|
|
109
|
+
),
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
await refresh()
|
|
113
|
+
}, [pendingActionId, phase, props.endpoint, refresh, t])
|
|
114
|
+
|
|
115
|
+
const handleCancel = React.useCallback(async () => {
|
|
116
|
+
if (!pendingActionId) return
|
|
117
|
+
const result = await cancelPendingAction(pendingActionId, {
|
|
118
|
+
endpoint: props.endpoint,
|
|
119
|
+
})
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
setConfirmError(result.error)
|
|
122
|
+
}
|
|
123
|
+
await refresh()
|
|
124
|
+
}, [pendingActionId, props.endpoint, refresh])
|
|
125
|
+
|
|
126
|
+
const currentStatus = action?.status ?? null
|
|
127
|
+
const executionError = action?.executionResult?.error
|
|
128
|
+
// Treat any captured handler error as terminal too — the dispatcher
|
|
129
|
+
// sometimes returns the action before the row has fully transitioned
|
|
130
|
+
// out of `executing`, and we must never leave the spinner masking a
|
|
131
|
+
// real failure. The MutationResultCard's failure path renders the
|
|
132
|
+
// error envelope as long as `executionResult.error` is set.
|
|
133
|
+
const isTerminal =
|
|
134
|
+
currentStatus === 'confirmed' ||
|
|
135
|
+
currentStatus === 'failed' ||
|
|
136
|
+
currentStatus === 'cancelled' ||
|
|
137
|
+
currentStatus === 'expired' ||
|
|
138
|
+
Boolean(executionError)
|
|
139
|
+
|
|
140
|
+
const { handleKeyDown } = useAiShortcuts({
|
|
141
|
+
onSubmit: () => {
|
|
142
|
+
void handleConfirm()
|
|
143
|
+
},
|
|
144
|
+
onCancel: () => {
|
|
145
|
+
void handleCancel()
|
|
146
|
+
},
|
|
147
|
+
enabled: phase === 'preview' && !isTerminal,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Terminal — short-circuit into the result card.
|
|
151
|
+
if (isTerminal && action) {
|
|
152
|
+
return (
|
|
153
|
+
<MutationResultCard
|
|
154
|
+
componentId="mutation-result-card"
|
|
155
|
+
pendingActionId={pendingActionId}
|
|
156
|
+
initialAction={action}
|
|
157
|
+
endpoint={props.endpoint}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Confirming — flip to the spinner card. Propagate the confirmError so
|
|
163
|
+
// the user sees the structured envelope even though the confirm call has
|
|
164
|
+
// already resolved.
|
|
165
|
+
if (phase === 'confirming') {
|
|
166
|
+
return (
|
|
167
|
+
<ConfirmationCard
|
|
168
|
+
componentId="confirmation-card"
|
|
169
|
+
pendingActionId={pendingActionId}
|
|
170
|
+
initialAction={action ?? undefined}
|
|
171
|
+
endpoint={props.endpoint}
|
|
172
|
+
payload={{
|
|
173
|
+
sideEffectsSummary: action?.sideEffectsSummary ?? null,
|
|
174
|
+
pendingAction: action ?? undefined,
|
|
175
|
+
confirmError: confirmError ?? undefined,
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const batch = Array.isArray(action?.records) && action!.records!.length > 0 ? action!.records! : null
|
|
182
|
+
const summary = batch ? summarizeBatch(batch) : null
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<section
|
|
186
|
+
className="rounded-md border border-border bg-background p-4 text-sm outline-none"
|
|
187
|
+
tabIndex={0}
|
|
188
|
+
onKeyDown={handleKeyDown}
|
|
189
|
+
data-ai-mutation-preview
|
|
190
|
+
data-ai-mutation-preview-mode={batch ? 'batch' : 'single'}
|
|
191
|
+
>
|
|
192
|
+
<header className="flex items-start justify-between gap-3">
|
|
193
|
+
<div className="flex items-start gap-2">
|
|
194
|
+
<ShieldAlert className="mt-0.5 size-4 text-status-warning-icon" aria-hidden />
|
|
195
|
+
<div>
|
|
196
|
+
<h4 className="text-sm font-semibold">
|
|
197
|
+
{t(
|
|
198
|
+
'ai_assistant.chat.mutation_cards.preview.title',
|
|
199
|
+
'Review proposed changes',
|
|
200
|
+
)}
|
|
201
|
+
</h4>
|
|
202
|
+
{action?.sideEffectsSummary ? (
|
|
203
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
204
|
+
{action.sideEffectsSummary}
|
|
205
|
+
</p>
|
|
206
|
+
) : null}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</header>
|
|
210
|
+
|
|
211
|
+
<div className="mt-3" data-ai-mutation-preview-body>
|
|
212
|
+
{summary ? (
|
|
213
|
+
<div
|
|
214
|
+
className="rounded-md border border-border bg-muted/30 p-3 text-sm"
|
|
215
|
+
data-ai-mutation-preview-batch-summary
|
|
216
|
+
>
|
|
217
|
+
<p className="font-medium">
|
|
218
|
+
{t(
|
|
219
|
+
'ai_assistant.chat.mutation_cards.preview.batchSummary',
|
|
220
|
+
'Batch update',
|
|
221
|
+
)}
|
|
222
|
+
{': '}
|
|
223
|
+
<span data-ai-mutation-preview-count>{summary.count}</span>{' '}
|
|
224
|
+
<span>
|
|
225
|
+
{t(
|
|
226
|
+
'ai_assistant.chat.mutation_cards.preview.batchRecords',
|
|
227
|
+
'records',
|
|
228
|
+
)}
|
|
229
|
+
</span>
|
|
230
|
+
</p>
|
|
231
|
+
{summary.labels.length > 0 ? (
|
|
232
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
233
|
+
{summary.labels.join(', ')}
|
|
234
|
+
{summary.count > summary.labels.length
|
|
235
|
+
? ` +${summary.count - summary.labels.length}`
|
|
236
|
+
: ''}
|
|
237
|
+
</p>
|
|
238
|
+
) : null}
|
|
239
|
+
</div>
|
|
240
|
+
) : (
|
|
241
|
+
<FieldDiffCard fieldDiff={action?.fieldDiff ?? null} />
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{expanded ? (
|
|
246
|
+
<div className="mt-3 rounded-md border border-border bg-muted/20 p-3" data-ai-mutation-preview-details>
|
|
247
|
+
<FieldDiffCard
|
|
248
|
+
fieldDiff={action?.fieldDiff ?? null}
|
|
249
|
+
records={action?.records ?? null}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
) : null}
|
|
253
|
+
|
|
254
|
+
<div className="mt-3 flex items-center justify-between gap-2">
|
|
255
|
+
<Button
|
|
256
|
+
type="button"
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="sm"
|
|
259
|
+
onClick={() => setExpanded((value) => !value)}
|
|
260
|
+
data-ai-mutation-preview-review
|
|
261
|
+
>
|
|
262
|
+
<Eye className="size-4" aria-hidden />
|
|
263
|
+
<span>
|
|
264
|
+
{t(
|
|
265
|
+
'ai_assistant.chat.mutation_cards.preview.reviewDetails',
|
|
266
|
+
'Review details',
|
|
267
|
+
)}
|
|
268
|
+
</span>
|
|
269
|
+
<ChevronDown
|
|
270
|
+
className={`size-4 transition-transform ${expanded ? 'rotate-180' : ''}`}
|
|
271
|
+
aria-hidden
|
|
272
|
+
/>
|
|
273
|
+
</Button>
|
|
274
|
+
<div className="flex items-center gap-2">
|
|
275
|
+
<Button
|
|
276
|
+
type="button"
|
|
277
|
+
variant="outline"
|
|
278
|
+
size="sm"
|
|
279
|
+
onClick={() => {
|
|
280
|
+
void handleCancel()
|
|
281
|
+
}}
|
|
282
|
+
data-ai-mutation-preview-cancel
|
|
283
|
+
>
|
|
284
|
+
{t('ai_assistant.chat.mutation_cards.preview.cancel', 'Cancel')}
|
|
285
|
+
</Button>
|
|
286
|
+
<Button
|
|
287
|
+
type="button"
|
|
288
|
+
size="sm"
|
|
289
|
+
onClick={() => {
|
|
290
|
+
void handleConfirm()
|
|
291
|
+
}}
|
|
292
|
+
data-ai-mutation-preview-confirm
|
|
293
|
+
>
|
|
294
|
+
{t('ai_assistant.chat.mutation_cards.preview.confirm', 'Confirm')}
|
|
295
|
+
</Button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</section>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export default MutationPreviewCard
|