@voyantjs/workflows-react 0.106.0 → 0.107.0

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.
Files changed (63) hide show
  1. package/README.md +43 -0
  2. package/dist/client.d.ts +2 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +1 -0
  5. package/dist/components/common.d.ts +30 -0
  6. package/dist/components/common.d.ts.map +1 -0
  7. package/dist/components/common.js +108 -0
  8. package/dist/components/workflow-run-actions.d.ts +7 -0
  9. package/dist/components/workflow-run-actions.d.ts.map +1 -0
  10. package/dist/components/workflow-run-actions.js +72 -0
  11. package/dist/components/workflow-run-detail-page.d.ts +10 -0
  12. package/dist/components/workflow-run-detail-page.d.ts.map +1 -0
  13. package/dist/components/workflow-run-detail-page.js +96 -0
  14. package/dist/components/workflow-runs-filters.d.ts +32 -0
  15. package/dist/components/workflow-runs-filters.d.ts.map +1 -0
  16. package/dist/components/workflow-runs-filters.js +97 -0
  17. package/dist/components/workflow-runs-page.d.ts +12 -0
  18. package/dist/components/workflow-runs-page.d.ts.map +1 -0
  19. package/dist/components/workflow-runs-page.js +132 -0
  20. package/dist/components/workflow-schedules-page.d.ts +21 -0
  21. package/dist/components/workflow-schedules-page.d.ts.map +1 -0
  22. package/dist/components/workflow-schedules-page.js +144 -0
  23. package/dist/i18n/en.d.ts +4 -0
  24. package/dist/i18n/en.d.ts.map +1 -0
  25. package/dist/i18n/en.js +135 -0
  26. package/dist/i18n/index.d.ts +5 -0
  27. package/dist/i18n/index.d.ts.map +1 -0
  28. package/dist/i18n/index.js +3 -0
  29. package/dist/i18n/messages.d.ts +125 -0
  30. package/dist/i18n/messages.d.ts.map +1 -0
  31. package/dist/i18n/messages.js +1 -0
  32. package/dist/i18n/provider.d.ts +26 -0
  33. package/dist/i18n/provider.d.ts.map +1 -0
  34. package/dist/i18n/provider.js +44 -0
  35. package/dist/i18n/ro.d.ts +4 -0
  36. package/dist/i18n/ro.d.ts.map +1 -0
  37. package/dist/i18n/ro.js +137 -0
  38. package/dist/schedules-client.d.ts +2 -0
  39. package/dist/schedules-client.d.ts.map +1 -0
  40. package/dist/schedules-client.js +1 -0
  41. package/dist/types.d.ts +2 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +1 -0
  44. package/dist/ui.d.ts +9 -0
  45. package/dist/ui.d.ts.map +1 -0
  46. package/dist/ui.js +6 -0
  47. package/package.json +69 -21
  48. package/src/client.ts +4 -0
  49. package/src/components/common.tsx +182 -0
  50. package/src/components/workflow-run-actions.tsx +160 -0
  51. package/src/components/workflow-run-detail-page.tsx +393 -0
  52. package/src/components/workflow-runs-filters.tsx +349 -0
  53. package/src/components/workflow-runs-page.tsx +357 -0
  54. package/src/components/workflow-schedules-page.tsx +398 -0
  55. package/src/i18n/en.ts +142 -0
  56. package/src/i18n/index.ts +26 -0
  57. package/src/i18n/messages.ts +125 -0
  58. package/src/i18n/provider.tsx +96 -0
  59. package/src/i18n/ro.ts +144 -0
  60. package/src/schedules-client.ts +8 -0
  61. package/src/styles.css +11 -0
  62. package/src/types.ts +14 -0
  63. package/src/ui.ts +63 -0
@@ -0,0 +1,160 @@
1
+ "use client"
2
+
3
+ import { Button } from "@voyantjs/ui/components/button"
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card"
5
+ import { AlertTriangle, Play, RotateCw } from "lucide-react"
6
+ import { useCallback, useState } from "react"
7
+
8
+ import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js"
9
+ import type { WorkflowRun, WorkflowRunActionError, WorkflowRunsApi } from "../types.js"
10
+ import { runHasLiveStatus } from "./common.js"
11
+
12
+ export function WorkflowRunActionsCard({
13
+ api,
14
+ run,
15
+ onOpenRun,
16
+ }: {
17
+ api: WorkflowRunsApi
18
+ run: WorkflowRun
19
+ onOpenRun?: (id: string) => void
20
+ }) {
21
+ const messages = useWorkflowRunsUiMessagesOrDefault()
22
+ const [busy, setBusy] = useState<"rerun" | "resume" | null>(null)
23
+ const [feedback, setFeedback] = useState<{ kind: "info" | "error"; message: string } | null>(null)
24
+ const [confirmOpen, setConfirmOpen] = useState(false)
25
+
26
+ const explainError = useCallback(
27
+ (err: WorkflowRunActionError): string => {
28
+ if (err.error === "runner_not_registered") return messages.actions.runnerMissing
29
+ if (err.error === "rerun_blocked") return err.detail ?? messages.actions.rerunBlocked
30
+ if (err.error === "incomplete_prior_step") {
31
+ return err.detail ?? messages.actions.incompletePriorStep
32
+ }
33
+ return err.detail ?? err.error ?? messages.actions.actionFailed
34
+ },
35
+ [messages],
36
+ )
37
+
38
+ const doRerun = async (confirm: boolean): Promise<void> => {
39
+ setBusy("rerun")
40
+ setFeedback(null)
41
+ try {
42
+ const result = await api.rerunRun(run.id, { confirm })
43
+ if (result.ok) {
44
+ setFeedback({ kind: "info", message: messages.actions.rerunStarted })
45
+ onOpenRun?.(result.data.runId)
46
+ setConfirmOpen(false)
47
+ } else if (result.error.error === "confirmation_required") {
48
+ setConfirmOpen(true)
49
+ } else {
50
+ setFeedback({ kind: "error", message: explainError(result.error) })
51
+ }
52
+ } finally {
53
+ setBusy(null)
54
+ }
55
+ }
56
+
57
+ const doResume = async (): Promise<void> => {
58
+ setBusy("resume")
59
+ setFeedback(null)
60
+ try {
61
+ const result = await api.resumeRun(run.id)
62
+ if (result.ok) {
63
+ setFeedback({
64
+ kind: "info",
65
+ message: messages.actions.resumeStarted(result.data.resumeFromStep ?? ""),
66
+ })
67
+ onOpenRun?.(result.data.runId)
68
+ } else {
69
+ setFeedback({ kind: "error", message: explainError(result.error) })
70
+ }
71
+ } finally {
72
+ setBusy(null)
73
+ }
74
+ }
75
+
76
+ const canResume = run.status === "failed"
77
+ const isRunning = runHasLiveStatus(run)
78
+
79
+ return (
80
+ <Card>
81
+ <CardContent className="flex flex-wrap items-center gap-2 pt-4">
82
+ <Button
83
+ variant="default"
84
+ size="sm"
85
+ onClick={() => void doRerun(false)}
86
+ disabled={busy !== null || isRunning}
87
+ title={isRunning ? messages.actions.waitForCompletion : messages.actions.rerunDescription}
88
+ >
89
+ <RotateCw className={`mr-1.5 h-3.5 w-3.5 ${busy === "rerun" ? "animate-spin" : ""}`} />
90
+ {busy === "rerun" ? messages.actions.rerunBusy : messages.actions.rerun}
91
+ </Button>
92
+ <Button
93
+ variant="secondary"
94
+ size="sm"
95
+ onClick={() => void doResume()}
96
+ disabled={busy !== null || !canResume}
97
+ title={
98
+ canResume ? messages.actions.resumeDescription : messages.actions.resumeUnavailable
99
+ }
100
+ >
101
+ <Play className={`mr-1.5 h-3.5 w-3.5 ${busy === "resume" ? "animate-pulse" : ""}`} />
102
+ {busy === "resume" ? messages.actions.resumeBusy : messages.actions.resume}
103
+ </Button>
104
+ {feedback ? (
105
+ <span
106
+ className={`ml-auto text-xs ${
107
+ feedback.kind === "error" ? "text-destructive" : "text-muted-foreground"
108
+ }`}
109
+ >
110
+ {feedback.message}
111
+ </span>
112
+ ) : null}
113
+ </CardContent>
114
+ {confirmOpen ? (
115
+ <ConfirmRerunDialog
116
+ onCancel={() => setConfirmOpen(false)}
117
+ onConfirm={() => void doRerun(true)}
118
+ busy={busy === "rerun"}
119
+ />
120
+ ) : null}
121
+ </Card>
122
+ )
123
+ }
124
+
125
+ function ConfirmRerunDialog({
126
+ onCancel,
127
+ onConfirm,
128
+ busy,
129
+ }: {
130
+ onCancel: () => void
131
+ onConfirm: () => void
132
+ busy: boolean
133
+ }) {
134
+ const messages = useWorkflowRunsUiMessagesOrDefault()
135
+ return (
136
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
137
+ <Card className="w-full max-w-md border-amber-500/40">
138
+ <CardHeader className="pb-3">
139
+ <CardTitle className="flex items-center gap-2 text-base">
140
+ <AlertTriangle className="h-4 w-4 text-amber-500" />
141
+ {messages.actions.confirmTitle}
142
+ </CardTitle>
143
+ </CardHeader>
144
+ <CardContent className="space-y-3 text-sm">
145
+ <p>{messages.actions.confirmBody}</p>
146
+ <p className="text-muted-foreground text-xs">{messages.actions.confirmTip}</p>
147
+ <div className="flex justify-end gap-2 pt-2">
148
+ <Button variant="ghost" size="sm" onClick={onCancel} disabled={busy}>
149
+ {messages.actions.cancel}
150
+ </Button>
151
+ <Button variant="default" size="sm" onClick={onConfirm} disabled={busy}>
152
+ <RotateCw className={`mr-1.5 h-3.5 w-3.5 ${busy ? "animate-spin" : ""}`} />
153
+ {messages.actions.rerunAnyway}
154
+ </Button>
155
+ </div>
156
+ </CardContent>
157
+ </Card>
158
+ </div>
159
+ )
160
+ }
@@ -0,0 +1,393 @@
1
+ "use client"
2
+
3
+ import { Badge } from "@voyantjs/ui/components/badge"
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card"
5
+ import { ChevronDown, ChevronRight, Link2 } from "lucide-react"
6
+ import { useEffect, useMemo, useState } from "react"
7
+
8
+ import { useWorkflowRunsUiMessagesOrDefault } from "../i18n/index.js"
9
+ import type {
10
+ WorkflowRun,
11
+ WorkflowRunErrorPayload,
12
+ WorkflowRunStep,
13
+ WorkflowRunsApi,
14
+ } from "../types.js"
15
+ import {
16
+ CopyableId,
17
+ formatDuration,
18
+ formatRelative,
19
+ PayloadBlock,
20
+ StatusBadge,
21
+ StatusIcon,
22
+ StepStatusIcon,
23
+ TagChip,
24
+ } from "./common.js"
25
+ import { WorkflowRunActionsCard } from "./workflow-run-actions.js"
26
+
27
+ export interface WorkflowRunDetailPageProps {
28
+ api: WorkflowRunsApi
29
+ runId: string
30
+ onOpenRun?: (id: string) => void
31
+ pollIntervalMs?: number
32
+ className?: string
33
+ }
34
+
35
+ export function WorkflowRunDetailPage({
36
+ api,
37
+ runId,
38
+ onOpenRun,
39
+ pollIntervalMs = 3000,
40
+ className,
41
+ }: WorkflowRunDetailPageProps) {
42
+ const messages = useWorkflowRunsUiMessagesOrDefault()
43
+ const [run, setRun] = useState<WorkflowRun | null>(null)
44
+ const [steps, setSteps] = useState<WorkflowRunStep[]>([])
45
+ const [error, setError] = useState<string | null>(null)
46
+ const [reruns, setReruns] = useState<WorkflowRun[]>([])
47
+
48
+ useEffect(() => {
49
+ let cancelled = false
50
+ const refresh = async () => {
51
+ try {
52
+ const [detail, children] = await Promise.all([
53
+ api.getRun(runId),
54
+ api.listRuns({ parentRunId: runId, limit: 20 }).catch(() => ({ data: [] })),
55
+ ])
56
+ if (cancelled) return
57
+ setRun(detail.data.run)
58
+ setSteps(detail.data.steps)
59
+ setReruns(children.data)
60
+ setError(null)
61
+ } catch (err) {
62
+ if (!cancelled) setError(err instanceof Error ? err.message : String(err))
63
+ }
64
+ }
65
+ void refresh()
66
+ const interval = setInterval(() => void refresh(), pollIntervalMs)
67
+ return () => {
68
+ cancelled = true
69
+ clearInterval(interval)
70
+ }
71
+ }, [api, pollIntervalMs, runId])
72
+
73
+ const duplicateRunError = useMemo(() => {
74
+ if (!run?.error) return false
75
+ return steps.some((step) => step.error?.message === run.error?.message)
76
+ }, [run?.error, steps])
77
+
78
+ if (error) return <ErrorMessage message={error} />
79
+ if (!run) {
80
+ return (
81
+ <Card className={className}>
82
+ <CardContent className="pt-4 text-muted-foreground text-sm">
83
+ {messages.page.loading}
84
+ </CardContent>
85
+ </Card>
86
+ )
87
+ }
88
+
89
+ return (
90
+ <div className={`space-y-4 ${className ?? ""}`}>
91
+ <RunHeaderCard run={run} onOpenRun={onOpenRun} />
92
+ <WorkflowRunActionsCard api={api} run={run} onOpenRun={onOpenRun} />
93
+ {reruns.length > 0 ? <RerunsListCard reruns={reruns} onOpenRun={onOpenRun} /> : null}
94
+ <StepsCard steps={steps} />
95
+ {run.input ? <PayloadCard title={messages.detail.input} value={run.input} /> : null}
96
+ {run.result ? <PayloadCard title={messages.detail.result} value={run.result} /> : null}
97
+ {run.error && !duplicateRunError ? (
98
+ <ErrorCard title={messages.detail.runError} error={run.error} />
99
+ ) : null}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ function RunHeaderCard({ run, onOpenRun }: { run: WorkflowRun; onOpenRun?: (id: string) => void }) {
105
+ const messages = useWorkflowRunsUiMessagesOrDefault()
106
+ return (
107
+ <Card>
108
+ <CardHeader className="space-y-3 pb-4">
109
+ <div className="flex flex-wrap items-center gap-3">
110
+ <StatusIcon status={run.status} />
111
+ <CardTitle className="font-semibold text-lg">{run.workflowName}</CardTitle>
112
+ <StatusBadge status={run.status} messages={messages} />
113
+ {run.durationMs != null ? (
114
+ <Badge variant="outline" className="font-mono text-xs">
115
+ {formatDuration(run.durationMs)}
116
+ </Badge>
117
+ ) : null}
118
+ {run.resumeFromStep ? (
119
+ <Badge variant="outline" className="font-mono text-xs">
120
+ {messages.detail.resumedAt(run.resumeFromStep)}
121
+ </Badge>
122
+ ) : null}
123
+ <CopyableId id={run.id} copiedLabel={messages.detail.copied} className="ml-auto" />
124
+ </div>
125
+ <div className="text-muted-foreground text-sm">
126
+ <span>
127
+ {messages.detail.started} {new Date(run.startedAt).toLocaleString()}
128
+ </span>
129
+ {run.completedAt ? (
130
+ <span>
131
+ {" · "}
132
+ {messages.detail.finished} {new Date(run.completedAt).toLocaleString()}
133
+ </span>
134
+ ) : null}
135
+ </div>
136
+ </CardHeader>
137
+ <CardContent className="space-y-3 pt-0">
138
+ <dl className="grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2">
139
+ <DefRow label={messages.detail.trigger} value={run.trigger} mono />
140
+ {run.correlationId ? (
141
+ <DefRow label={messages.detail.correlation} value={run.correlationId} mono />
142
+ ) : null}
143
+ {run.parentRunId ? (
144
+ <LinkedRunRow
145
+ label={messages.detail.parent}
146
+ runId={run.parentRunId}
147
+ onOpenRun={onOpenRun}
148
+ />
149
+ ) : null}
150
+ {run.triggeredByUserId ? (
151
+ <DefRow label={messages.detail.triggeredBy} value={run.triggeredByUserId} mono />
152
+ ) : null}
153
+ </dl>
154
+ {run.tags.length > 0 ? (
155
+ <div>
156
+ <div className="mb-1.5 text-muted-foreground text-xs uppercase tracking-wide">
157
+ {messages.detail.tags}
158
+ </div>
159
+ <div className="flex flex-wrap gap-1">
160
+ {run.tags.map((tag) => (
161
+ <TagChip key={tag} tag={tag} />
162
+ ))}
163
+ </div>
164
+ </div>
165
+ ) : null}
166
+ </CardContent>
167
+ </Card>
168
+ )
169
+ }
170
+
171
+ function LinkedRunRow({
172
+ label,
173
+ runId,
174
+ onOpenRun,
175
+ }: {
176
+ label: string
177
+ runId: string
178
+ onOpenRun?: (id: string) => void
179
+ }) {
180
+ return (
181
+ <div className="flex items-baseline gap-2">
182
+ <dt className="shrink-0 text-muted-foreground text-xs uppercase tracking-wide">{label}</dt>
183
+ <dd className="min-w-0 truncate">
184
+ <button
185
+ type="button"
186
+ onClick={() => onOpenRun?.(runId)}
187
+ className="inline-flex items-center gap-1.5 truncate font-mono text-xs hover:underline"
188
+ title={runId}
189
+ >
190
+ <Link2 className="h-3 w-3 opacity-60" />
191
+ {runId}
192
+ </button>
193
+ </dd>
194
+ </div>
195
+ )
196
+ }
197
+
198
+ function DefRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
199
+ return (
200
+ <div className="flex items-baseline gap-2">
201
+ <dt className="shrink-0 text-muted-foreground text-xs uppercase tracking-wide">{label}</dt>
202
+ <dd
203
+ className={
204
+ mono ? "min-w-0 truncate font-mono text-xs" : "min-w-0 truncate text-sm" // i18n-literal-ok: CSS classes
205
+ }
206
+ >
207
+ {value}
208
+ </dd>
209
+ </div>
210
+ )
211
+ }
212
+
213
+ function RerunsListCard({
214
+ reruns,
215
+ onOpenRun,
216
+ }: {
217
+ reruns: WorkflowRun[]
218
+ onOpenRun?: (id: string) => void
219
+ }) {
220
+ const messages = useWorkflowRunsUiMessagesOrDefault()
221
+ return (
222
+ <Card>
223
+ <CardHeader className="pb-3">
224
+ <CardTitle className="flex items-center gap-2 text-sm">
225
+ <Link2 className="h-3.5 w-3.5 text-muted-foreground" />
226
+ {messages.detail.reruns}
227
+ <span className="font-normal text-muted-foreground text-xs">{`(${reruns.length})`}</span>
228
+ </CardTitle>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <ul className="space-y-1">
232
+ {reruns.map((run) => (
233
+ <li key={run.id}>
234
+ <button
235
+ type="button"
236
+ onClick={() => onOpenRun?.(run.id)}
237
+ className="flex w-full items-center gap-2 rounded p-2 text-left text-sm hover:bg-muted/40"
238
+ >
239
+ <StatusIcon status={run.status} />
240
+ <span className="font-mono text-xs">{run.id}</span>
241
+ <span className="text-muted-foreground text-xs">{run.trigger}</span>
242
+ {run.resumeFromStep ? (
243
+ <Badge variant="outline" className="font-mono text-[10px]">
244
+ {messages.detail.resumedAt(run.resumeFromStep)}
245
+ </Badge>
246
+ ) : null}
247
+ <span className="ml-auto whitespace-nowrap text-muted-foreground text-xs">
248
+ {formatRelative(run.startedAt, messages)}
249
+ </span>
250
+ </button>
251
+ </li>
252
+ ))}
253
+ </ul>
254
+ </CardContent>
255
+ </Card>
256
+ )
257
+ }
258
+
259
+ function StepsCard({ steps }: { steps: WorkflowRunStep[] }) {
260
+ const messages = useWorkflowRunsUiMessagesOrDefault()
261
+ return (
262
+ <Card>
263
+ <CardHeader className="pb-3">
264
+ <CardTitle className="text-sm">{messages.detail.steps}</CardTitle>
265
+ </CardHeader>
266
+ <CardContent>
267
+ {steps.length === 0 ? (
268
+ <p className="text-muted-foreground text-sm">{messages.detail.noSteps}</p>
269
+ ) : (
270
+ <ol className="space-y-2">
271
+ {steps.map((step) => (
272
+ <StepRow key={step.id} step={step} />
273
+ ))}
274
+ </ol>
275
+ )}
276
+ </CardContent>
277
+ </Card>
278
+ )
279
+ }
280
+
281
+ function StepRow({ step }: { step: WorkflowRunStep }) {
282
+ const messages = useWorkflowRunsUiMessagesOrDefault()
283
+ const [open, setOpen] = useState(step.status === "failed")
284
+ const hasDetail = step.output !== null || step.error !== null
285
+ return (
286
+ <li className="rounded-md border">
287
+ <button
288
+ type="button"
289
+ className="flex w-full items-center gap-2 p-3 text-left text-sm hover:bg-muted/30"
290
+ onClick={() => hasDetail && setOpen((prev) => !prev)}
291
+ disabled={!hasDetail}
292
+ >
293
+ <StepStatusIcon status={step.status} />
294
+ <span className="font-medium">{`${step.sequence}. ${step.stepName}`}</span>
295
+ {step.error ? (
296
+ <span className="truncate text-destructive text-xs">{step.error.message}</span>
297
+ ) : null}
298
+ <span className="ml-auto whitespace-nowrap text-muted-foreground text-xs">
299
+ {step.durationMs != null
300
+ ? formatDuration(step.durationMs)
301
+ : messages.detail.durationUnavailable}
302
+ </span>
303
+ {hasDetail ? (
304
+ open ? (
305
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
306
+ ) : (
307
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
308
+ )
309
+ ) : null}
310
+ </button>
311
+ {open && hasDetail ? (
312
+ <div className="space-y-3 border-t p-3">
313
+ {step.error ? <ErrorBlock error={step.error} /> : null}
314
+ {step.output ? (
315
+ <PayloadBlock title={messages.detail.output} value={step.output} messages={messages} />
316
+ ) : null}
317
+ </div>
318
+ ) : null}
319
+ </li>
320
+ )
321
+ }
322
+
323
+ function PayloadCard({ title, value }: { title: string; value: Record<string, unknown> }) {
324
+ const messages = useWorkflowRunsUiMessagesOrDefault()
325
+ return (
326
+ <Card>
327
+ <CardHeader className="pb-3">
328
+ <CardTitle className="text-sm">{title}</CardTitle>
329
+ </CardHeader>
330
+ <CardContent>
331
+ <PayloadBlock title={title} value={value} messages={messages} hideTitle />
332
+ </CardContent>
333
+ </Card>
334
+ )
335
+ }
336
+
337
+ function ErrorCard({ title, error }: { title: string; error: WorkflowRunErrorPayload }) {
338
+ return (
339
+ <Card className="border-destructive/40">
340
+ <CardHeader className="pb-3">
341
+ <CardTitle className="text-destructive text-sm">{title}</CardTitle>
342
+ </CardHeader>
343
+ <CardContent>
344
+ <ErrorBlock error={error} />
345
+ </CardContent>
346
+ </Card>
347
+ )
348
+ }
349
+
350
+ function ErrorBlock({ error }: { error: WorkflowRunErrorPayload }) {
351
+ const messages = useWorkflowRunsUiMessagesOrDefault()
352
+ const [stackOpen, setStackOpen] = useState(false)
353
+ return (
354
+ <div className="space-y-2">
355
+ <div className="rounded border border-destructive/30 bg-destructive/5 p-3 text-sm">
356
+ <div className="font-medium text-destructive">{error.message}</div>
357
+ {error.code || error.stepName ? (
358
+ <div className="mt-1 flex flex-wrap gap-2 text-muted-foreground text-xs">
359
+ {error.code ? <span>{`${messages.detail.code} ${error.code}`}</span> : null}
360
+ {error.stepName ? <span>{`${messages.detail.step} ${error.stepName}`}</span> : null}
361
+ </div>
362
+ ) : null}
363
+ </div>
364
+ {error.stack ? (
365
+ <details
366
+ open={stackOpen}
367
+ onToggle={(event) => setStackOpen((event.target as HTMLDetailsElement).open)}
368
+ className="rounded border bg-muted/30"
369
+ >
370
+ <summary className="flex cursor-pointer select-none items-center gap-1.5 px-3 py-2 text-muted-foreground text-xs hover:text-foreground">
371
+ {stackOpen ? (
372
+ <ChevronDown className="h-3.5 w-3.5" />
373
+ ) : (
374
+ <ChevronRight className="h-3.5 w-3.5" />
375
+ )}
376
+ {messages.detail.stackTrace}
377
+ </summary>
378
+ <pre className="overflow-x-auto whitespace-pre p-3 pt-0 font-mono text-[11px] leading-relaxed text-muted-foreground">
379
+ {error.stack}
380
+ </pre>
381
+ </details>
382
+ ) : null}
383
+ </div>
384
+ )
385
+ }
386
+
387
+ function ErrorMessage({ message }: { message: string }) {
388
+ return (
389
+ <Card className="border-destructive/40">
390
+ <CardContent className="pt-4 text-destructive text-sm">{message}</CardContent>
391
+ </Card>
392
+ )
393
+ }