chore: import clean Agent Desk snapshot (6/7)

This commit is contained in:
2026-10-16 12:02:48 +00:00
parent d215bf6303
commit 756ae33d05
278 changed files with 53808 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
"use client"
import { useCallback, useEffect, useRef } from "react"
import { useI18n } from "@/i18n/provider"
import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin"
const MESSAGE_SOURCE = "agent-desk"
const EMPTY_NODE_SPECS: AIWorkflowNodeSpec[] = []
function definitionsEqual(
left: AIWorkflowDefinition,
right: AIWorkflowDefinition
) {
return JSON.stringify(left) === JSON.stringify(right)
}
type EditorMessage =
| {
source: typeof MESSAGE_SOURCE
type: "workflow:ready"
}
| {
source: typeof MESSAGE_SOURCE
type: "workflow:change"
document: AIWorkflowDefinition
}
export function OfficialWorkflowEditor({
documentKey,
definition,
nodeSpecs = EMPTY_NODE_SPECS,
onDefinitionChange,
readonly = false,
}: {
documentKey: string
definition: AIWorkflowDefinition
nodeSpecs?: AIWorkflowNodeSpec[]
onDefinitionChange: (definition: AIWorkflowDefinition) => void
readonly?: boolean
}) {
const t = useI18n()
const frameRef = useRef<HTMLIFrameElement>(null)
const definitionRef = useRef(definition)
useEffect(() => {
definitionRef.current = definition
}, [definition])
const loadDocument = useCallback(() => {
frameRef.current?.contentWindow?.postMessage(
{
source: MESSAGE_SOURCE,
type: "workflow:load",
documentKey,
document: definitionRef.current,
nodeSpecs,
readonly,
},
window.location.origin
)
}, [documentKey, nodeSpecs, readonly])
useEffect(() => {
const handleMessage = (event: MessageEvent<EditorMessage>) => {
if (
event.origin !== window.location.origin ||
event.source !== frameRef.current?.contentWindow ||
event.data?.source !== MESSAGE_SOURCE
) {
return
}
if (event.data.type === "workflow:ready") {
loadDocument()
} else if (event.data.type === "workflow:change") {
if (definitionsEqual(event.data.document, definitionRef.current)) {
return
}
definitionRef.current = event.data.document
onDefinitionChange(event.data.document)
}
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [loadDocument, onDefinitionChange])
useEffect(() => {
loadDocument()
}, [loadDocument])
return (
<iframe
ref={frameRef}
src="/flowgram-editor/index.html"
title={t("aiWorkflow.editorTitle")}
className="h-full min-h-[560px] w-full border-0 bg-white"
/>
)
}

View File

@@ -0,0 +1,84 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { ProjectDialog } from "@/components/project-dialog"
import { Badge } from "@/components/ui/badge"
import { useI18n } from "@/i18n/provider"
import {
fetchAIWorkflowUsage,
type AIWorkflow,
type AIWorkflowUsage,
} from "@/lib/api/admin"
export function WorkflowUsageDialog({
workflow,
open,
onOpenChange,
}: {
workflow: AIWorkflow | null
open: boolean
onOpenChange: (open: boolean) => void
}) {
const t = useI18n()
const [usage, setUsage] = useState<AIWorkflowUsage[]>([])
const [loading, setLoading] = useState(false)
const load = useCallback(async () => {
if (!workflow || !open) return
setLoading(true)
try {
setUsage((await fetchAIWorkflowUsage(workflow.id)) ?? [])
} catch (error) {
toast.error(error instanceof Error ? error.message : t("aiWorkflow.loadFailed"))
} finally {
setLoading(false)
}
}, [open, t, workflow])
useEffect(() => {
void load()
}, [load])
return (
<ProjectDialog
open={open}
onOpenChange={onOpenChange}
title={t("aiWorkflow.usageTitle")}
description={workflow?.name}
size="lg"
bodyClassName="min-h-[360px]"
>
{loading ? (
<div className="flex min-h-64 items-center justify-center text-sm text-muted-foreground">
{t("aiWorkflow.loadingUsage")}
</div>
) : usage.length ? (
<div className="space-y-3">
{usage.map((item) => (
<div
key={`${item.aiAgentId}-${item.workflowVersionId}`}
className="flex items-center justify-between rounded-md border p-4"
>
<div>
<div className="font-medium">{item.aiAgentName}</div>
<div className="mt-1 text-sm text-muted-foreground">
{t("aiWorkflow.fixedBoundVersion", { version: String(item.workflowVersion) })}
</div>
</div>
<Badge variant={item.enabled ? "secondary" : "outline"}>
{item.enabled ? t("aiWorkflow.enabledStatus") : t("aiWorkflow.disabledStatus")}
</Badge>
</div>
))}
</div>
) : (
<div className="flex min-h-64 items-center justify-center text-sm text-muted-foreground">
{t("aiWorkflow.notUsedByAnyAgent")}
</div>
)}
</ProjectDialog>
)
}

View File

@@ -0,0 +1,116 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { ProjectDialog } from "@/components/project-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { useI18n } from "@/i18n/provider"
import {
fetchAIWorkflowVersions,
restoreAIWorkflowVersion,
type AIWorkflow,
type AIWorkflowVersion,
} from "@/lib/api/admin"
import { formatDateTime } from "@/lib/utils"
export function WorkflowVersionsDialog({
workflow,
open,
onOpenChange,
onRestored,
}: {
workflow: AIWorkflow | null
open: boolean
onOpenChange: (open: boolean) => void
onRestored: () => void
}) {
const t = useI18n()
const [versions, setVersions] = useState<AIWorkflowVersion[]>([])
const [loading, setLoading] = useState(false)
const [restoringId, setRestoringId] = useState<number | null>(null)
const load = useCallback(async () => {
if (!workflow || !open) return
setLoading(true)
try {
const page = await fetchAIWorkflowVersions({
workflowId: workflow.id,
limit: 50,
})
setVersions(page.results ?? [])
} catch (error) {
toast.error(error instanceof Error ? error.message : t("aiWorkflow.loadFailed"))
} finally {
setLoading(false)
}
}, [open, t, workflow])
useEffect(() => {
void load()
}, [load])
async function restore(version: AIWorkflowVersion) {
if (!workflow || restoringId !== null) return
setRestoringId(version.id)
try {
await restoreAIWorkflowVersion(workflow.id, version.id)
onRestored()
toast.success(t("aiWorkflow.restoredDraftSuccess", { version: String(version.version) }))
} catch (error) {
toast.error(error instanceof Error ? error.message : t("aiWorkflow.restoreFailed"))
} finally {
setRestoringId(null)
}
}
return (
<ProjectDialog
open={open}
onOpenChange={onOpenChange}
title={t("aiWorkflow.versionsTitle")}
description={workflow?.name}
size="lg"
bodyClassName="min-h-[360px]"
>
{loading ? (
<div className="flex min-h-64 items-center justify-center text-sm text-muted-foreground">
{t("aiWorkflow.loadingVersions")}
</div>
) : versions.length ? (
<div className="space-y-3">
{versions.map((version) => (
<div
key={version.id}
className="flex items-center gap-4 rounded-md border p-4"
>
<Badge>v{version.version}</Badge>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{formatDateTime(version.publishedAt || version.createdAt)}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{t("aiWorkflow.publisher", { name: version.publishedByName || "-" })}
</div>
</div>
<Button
variant="outline"
size="sm"
disabled={restoringId !== null}
onClick={() => void restore(version)}
>
{restoringId === version.id ? t("aiWorkflow.restoring") : t("aiWorkflow.restoreToDraft")}
</Button>
</div>
))}
</div>
) : (
<div className="flex min-h-64 items-center justify-center text-sm text-muted-foreground">
{t("aiWorkflow.noVersionsYet")}
</div>
)}
</ProjectDialog>
)
}

View File

@@ -0,0 +1,436 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { PencilIcon } from "lucide-react"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { OptionCombobox } from "@/components/option-combobox"
import { useI18n } from "@/i18n/provider"
import {
createAIWorkflow,
fetchAIWorkflow,
fetchAIWorkflowDefaultDefinition,
fetchAIWorkflowNodeSpecs,
fetchAIWorkflowTemplates,
publishAIWorkflow,
updateAIWorkflow,
type AIWorkflow,
type AIWorkflowDefinition,
type AIWorkflowNodeSpec,
type AIWorkflowTemplate,
} from "@/lib/api/admin"
import { OfficialWorkflowEditor } from "./official-workflow-editor"
const emptyDefinition: AIWorkflowDefinition = {
schemaVersion: 2,
nodes: [
{
id: "start_1",
type: "start",
meta: { position: { x: 0, y: 80 } },
data: { title: "Start", config: {}, inputsValues: {} },
},
{
id: "end_1",
type: "end",
meta: { position: { x: 260, y: 80 } },
data: { title: "End", config: {}, inputsValues: {} },
},
],
edges: [
{
sourceNodeID: "start_1",
targetNodeID: "end_1",
sourcePortID: "edge_start_end",
},
],
}
type WorkflowWorkbenchProps = {
workflowID?: number
onSaved?: () => void
}
export function WorkflowWorkbench({
workflowID,
onSaved,
}: WorkflowWorkbenchProps) {
const t = useI18n()
const [active, setActive] = useState<AIWorkflow | null>(null)
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [definition, setDefinition] =
useState<AIWorkflowDefinition>(emptyDefinition)
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
const [templates, setTemplates] = useState<AIWorkflowTemplate[]>([])
const [selectedTemplate, setSelectedTemplate] = useState("")
const [saving, setSaving] = useState(false)
const [dirty, setDirty] = useState(false)
const [loaded, setLoaded] = useState(false)
const [editingField, setEditingField] = useState<
"name" | "description" | null
>(null)
const editSnapshotRef = useRef({ value: "", dirty: false })
const cancellingEditRef = useRef(false)
const savingRef = useRef(false)
const load = useCallback(async () => {
setLoaded(false)
const [specs, availableTemplates] = await Promise.all([
fetchAIWorkflowNodeSpecs(),
fetchAIWorkflowTemplates(),
])
setNodeSpecs(specs)
setTemplates(availableTemplates)
if (!workflowID) {
setDefinition(await fetchAIWorkflowDefaultDefinition())
setLoaded(true)
return
}
const item = await fetchAIWorkflow(workflowID)
setActive(item)
setName(item.name)
setDescription(item.description)
setDefinition(item.draftDefinition)
setDirty(false)
setLoaded(true)
}, [workflowID])
const handleDefinitionChange = useCallback(
(next: AIWorkflowDefinition) => {
setDefinition(next)
setDirty(true)
},
[]
)
useEffect(() => {
void load().catch((error) =>
toast.error(error instanceof Error ? error.message : t("aiWorkflow.loadFailed"))
)
}, [load, t])
function beginEditing(field: "name" | "description") {
cancellingEditRef.current = false
editSnapshotRef.current = {
value: field === "name" ? name : description,
dirty,
}
setEditingField(field)
}
async function finishEditing(field: "name" | "description") {
if (cancellingEditRef.current) {
cancellingEditRef.current = false
return
}
setEditingField(null)
const currentValue = field === "name" ? name.trim() : description.trim()
if (currentValue === editSnapshotRef.current.value) {
setDirty(editSnapshotRef.current.dirty)
return
}
if (field === "name" && !currentValue) {
setName(editSnapshotRef.current.value)
setDirty(editSnapshotRef.current.dirty)
toast.error(t("aiWorkflow.nameRequired"))
return
}
const nextName = field === "name" ? currentValue : name.trim()
const nextDescription =
field === "description" ? currentValue : description.trim()
if (savingRef.current) return
savingRef.current = true
setSaving(true)
try {
if (active) {
await updateAIWorkflow({
id: active.id,
name: nextName,
description: nextDescription,
definition: active.draftDefinition,
})
setActive((current) =>
current
? {
...current,
name: nextName,
description: nextDescription,
updatedAt: new Date().toISOString(),
}
: current
)
} else {
const created = await createAIWorkflow({
name: nextName,
description: nextDescription,
definition,
})
setActive(created)
}
setName(nextName)
setDescription(nextDescription)
setDirty(active ? editSnapshotRef.current.dirty : false)
onSaved?.()
toast.success(field === "name" ? t("aiWorkflow.nameSaved") : t("aiWorkflow.descriptionSaved"))
} catch (error) {
setDirty(true)
toast.error(error instanceof Error ? error.message : t("aiWorkflow.saveFailed"))
} finally {
savingRef.current = false
setSaving(false)
}
}
function cancelEditing(field: "name" | "description") {
cancellingEditRef.current = true
if (field === "name") {
setName(editSnapshotRef.current.value)
} else {
setDescription(editSnapshotRef.current.value)
}
setDirty(editSnapshotRef.current.dirty)
setEditingField(null)
}
async function save() {
if (savingRef.current) return
if (!name.trim()) {
toast.error(t("aiWorkflow.nameRequired"))
return
}
const savedName = name.trim()
const savedDescription = description.trim()
savingRef.current = true
setSaving(true)
try {
if (active) {
await updateAIWorkflow({
id: active.id,
name: savedName,
description: savedDescription,
definition,
})
setActive((current) =>
current
? {
...current,
name: savedName,
description: savedDescription,
draftDefinition: definition,
updatedAt: new Date().toISOString(),
}
: current
)
setName(savedName)
setDescription(savedDescription)
setDirty(false)
} else {
const created = await createAIWorkflow({
name: savedName,
description: savedDescription,
definition,
})
setActive(created)
setName(created.name)
setDescription(created.description)
setDirty(false)
}
onSaved?.()
toast.success(t("aiWorkflow.savedSuccess"))
} catch (error) {
toast.error(error instanceof Error ? error.message : t("aiWorkflow.saveFailed"))
} finally {
savingRef.current = false
setSaving(false)
}
}
async function publish() {
if (savingRef.current) return
if (!active) {
toast.error(t("aiWorkflow.saveFirst"))
return
}
savingRef.current = true
setSaving(true)
try {
const version = await publishAIWorkflow(active.id, definition)
setActive((current) =>
current
? {
...current,
draftDefinition: definition,
publishedVersionId: version.id,
updatedAt: version.updatedAt,
}
: current
)
setDirty(false)
onSaved?.()
toast.success(t("aiWorkflow.publishedVersionSuccess", { version: String(version.version) }))
} catch (error) {
toast.error(error instanceof Error ? error.message : t("aiWorkflow.publishFailed"))
} finally {
savingRef.current = false
setSaving(false)
}
}
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-background">
<header className="shrink-0 border-b px-4 py-1.5">
<div className="flex items-center gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{editingField === "name" ? (
<Input
autoFocus
aria-label={t("aiWorkflow.nameLabel")}
value={name}
onFocus={(event) => event.currentTarget.select()}
onChange={(event) => {
setName(event.target.value)
setDirty(true)
}}
onBlur={() => void finishEditing("name")}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault()
event.currentTarget.blur()
} else if (event.key === "Escape") {
event.preventDefault()
event.stopPropagation()
cancelEditing("name")
}
}}
className="h-7 min-w-48 max-w-xl flex-1 px-2 text-lg font-semibold"
/>
) : (
<button
type="button"
aria-label="Workflow Name"
onClick={() => beginEditing("name")}
className="group flex h-7 min-w-0 max-w-xl items-center gap-1.5 rounded-md px-1 text-left text-lg font-semibold hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="truncate">{name || t("aiWorkflow.createTitle")}</span>
<PencilIcon className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
</button>
)}
<Badge
variant={active?.publishedVersionId ? "secondary" : "outline"}
>
{active?.publishedVersionId ? t("aiWorkflow.published") : t("aiWorkflow.draft")}
</Badge>
{dirty ? (
<span className="text-xs text-amber-600">{t("supportHelpWorkbench.unsaved")}</span>
) : null}
</div>
{editingField === "description" ? (
<Input
autoFocus
aria-label="Workflow Description"
value={description}
onFocus={(event) => event.currentTarget.select()}
onChange={(event) => {
setDescription(event.target.value)
setDirty(true)
}}
onBlur={() => void finishEditing("description")}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault()
event.currentTarget.blur()
} else if (event.key === "Escape") {
event.preventDefault()
event.stopPropagation()
cancelEditing("description")
}
}}
placeholder="Workflow description..."
className="mt-0.5 h-6 max-w-2xl px-1 text-sm font-normal"
/>
) : (
<button
type="button"
aria-label="Workflow Description"
onClick={() => beginEditing("description")}
className="group mt-0.5 flex h-6 max-w-2xl items-center gap-1.5 rounded-md px-1 text-left text-sm text-muted-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="truncate">
{description || t("aiWorkflow.noDescription")}
</span>
<PencilIcon className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
</button>
)}
</div>
<div className="flex shrink-0 gap-2">
{!active ? (
<OptionCombobox
value={selectedTemplate}
placeholder="Template"
searchPlaceholder="Search template..."
emptyText="No templates"
triggerClassName="w-48"
options={templates.map((template) => ({
value: template.code,
label: template.name,
description: template.description,
}))}
onChange={(code) => {
const template = templates.find((item) => item.code === code)
if (!template) return
setSelectedTemplate(code)
setDefinition(structuredClone(template.definition))
setName(template.name)
setDescription(template.description)
setDirty(true)
}}
/>
) : null}
<Button
variant="outline"
disabled={saving}
onClick={() => void save()}
>
{t("common.save")}
</Button>
<Button
disabled={saving || !active}
onClick={() => void publish()}
>
{t("aiAgent.publishAgent")}
</Button>
</div>
<div className="w-8 shrink-0" aria-hidden="true" />
</div>
</header>
<div className="min-h-0 flex-1 overflow-hidden">
{loaded ? (
<OfficialWorkflowEditor
documentKey={
workflowID
? `workflow-${workflowID}`
: `new-${selectedTemplate || "blank"}`
}
definition={definition}
nodeSpecs={nodeSpecs}
onDefinitionChange={handleDefinitionChange}
/>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{t("aiWorkflow.loadingNodeCapabilities")}
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,168 @@
"use client"
import { useState } from "react"
import {
GitBranchIcon,
HistoryIcon,
UsersIcon,
} from "lucide-react"
import { DashboardCrudPage } from "@/components/dashboard/crud"
import { ProjectDialog } from "@/components/project-dialog"
import { Badge } from "@/components/ui/badge"
import {
deleteAIWorkflow,
fetchAIWorkflows,
type AIWorkflow,
type CreateAIWorkflowPayload,
} from "@/lib/api/admin"
import { useI18n } from "@/i18n/provider"
import { formatDateTime } from "@/lib/utils"
import { WorkflowUsageDialog } from "./_components/workflow-usage-dialog"
import { WorkflowVersionsDialog } from "./_components/workflow-versions-dialog"
import { WorkflowWorkbench } from "./_components/workflow-workbench"
export default function DashboardAIWorkflowsPage() {
const t = useI18n()
const [reloadKey, setReloadKey] = useState(0)
const [versionsWorkflow, setVersionsWorkflow] =
useState<AIWorkflow | null>(null)
const [usageWorkflow, setUsageWorkflow] = useState<AIWorkflow | null>(null)
return (
<>
<DashboardCrudPage<AIWorkflow, CreateAIWorkflowPayload>
filters={[
{
name: "name",
label: t("aiWorkflow.filterName"),
placeholder: t("aiWorkflow.searchName"),
defaultValue: "",
trim: true,
className: "w-full sm:w-72",
},
]}
columns={[
{
key: "workflow",
label: t("aiWorkflow.columnWorkflow"),
render: (item) => (
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
<GitBranchIcon className="size-4" />
</div>
<div className="min-w-0">
<div className="font-medium">{item.name}</div>
<div className="mt-1 line-clamp-2 text-sm text-muted-foreground">
{item.description || t("aiWorkflow.noDescription")}
</div>
</div>
</div>
),
},
{
key: "status",
label: t("aiWorkflow.columnStatus"),
render: (item) => (
<Badge
variant={item.publishedVersionId ? "secondary" : "outline"}
>
{item.publishedVersionId ? t("aiWorkflow.published") : t("aiWorkflow.draft")}
</Badge>
),
},
{
key: "version",
label: t("aiWorkflow.currentVersion"),
render: (item) =>
item.publishedVersionId
? t("aiWorkflow.publishedVersion", { version: item.publishedVersionId })
: "-",
},
{
key: "updatedAt",
label: t("common.actions"),
render: (item) => formatDateTime(item.updatedAt),
},
]}
fetchList={(query) =>
fetchAIWorkflows({
name: typeof query.name === "string" ? query.name : undefined,
page: Number(query.page),
limit: Number(query.limit),
})
}
getItemId={(item) => item.id}
createItem={async () => undefined}
updateItem={async () => undefined}
deleteItem={(item) => deleteAIWorkflow(item.id)}
rowActions={[
{
key: "versions",
label: t("aiWorkflow.versionHistory"),
icon: <HistoryIcon />,
run: ({ item }) => setVersionsWorkflow(item),
},
{
key: "usage",
label: t("aiWorkflow.usage"),
icon: <UsersIcon />,
run: ({ item }) => setUsageWorkflow(item),
},
]}
reloadKey={reloadKey}
renderEditDialog={({ open, item, itemId, onOpenChange }) => (
<ProjectDialog
open={open}
onOpenChange={onOpenChange}
title={item ? t("aiWorkflow.editTitle") : t("aiWorkflow.createTitle")}
defaultFullscreen
bodyScrollable={false}
headerClassName="sr-only"
>
<WorkflowWorkbench
workflowID={itemId ?? undefined}
onSaved={() => setReloadKey((value) => value + 1)}
/>
</ProjectDialog>
)}
labels={{
refresh: t("aiWorkflow.refresh"),
create: t("aiWorkflow.create"),
query: t("aiWorkflow.query"),
loading: t("aiWorkflow.loading"),
empty: t("aiWorkflow.empty"),
actions: t("aiWorkflow.actions"),
edit: t("aiWorkflow.edit"),
delete: t("aiWorkflow.delete"),
processing: t("aiWorkflow.processing"),
moreActions: (item) => t("aiWorkflow.moreActions", { name: item.name }),
loadFailed: t("aiWorkflow.loadFailed"),
saveFailed: t("aiWorkflow.saveFailed"),
deleteFailed: t("aiWorkflow.deleteFailed"),
created: (payload) => t("aiWorkflow.created", { name: payload.name }),
updated: (item) => t("aiWorkflow.updated", { name: item.name }),
deleted: (item) => t("aiWorkflow.deleted", { name: item.name }),
}}
/>
<WorkflowVersionsDialog
workflow={versionsWorkflow}
open={versionsWorkflow !== null}
onOpenChange={(open) => {
if (!open) setVersionsWorkflow(null)
}}
onRestored={() => setReloadKey((value) => value + 1)}
/>
<WorkflowUsageDialog
workflow={usageWorkflow}
open={usageWorkflow !== null}
onOpenChange={(open) => {
if (!open) setUsageWorkflow(null)
}}
/>
</>
)
}