fix(workflow): improve run audit interactions

This commit is contained in:
mlogclub
2026-06-24 11:03:36 +08:00
parent 26578705ad
commit dbd793f140
4 changed files with 179 additions and 50 deletions
+128 -9
View File
@@ -1,23 +1,34 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { useEffect, useMemo, useState, type ReactNode } from "react"
import {
AlertTriangleIcon,
Clock3Icon,
MessageCircleMoreIcon,
MessageSquareTextIcon,
WorkflowIcon,
} from "lucide-react"
import { toast } from "sonner"
import { ConversationDetailDialog } from "@/app/dashboard/conversation-monitor/_components/detail"
import { DashboardListPage } from "@/components/dashboard/list"
import { JsonTreeViewer } from "@/components/json-tree-viewer"
import { ProjectDialog } from "@/components/project-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import {
fetchAIAgentsAll,
fetchAIWorkflowRun,
fetchAIWorkflowRuns,
fetchConversationDetail,
fetchConversationMessages,
type AdminConversationDetail,
type AdminMessage,
type AIAgent,
type AIWorkflowNodeRun,
type AIWorkflowRun,
@@ -59,6 +70,10 @@ export default function DashboardAIWorkflowRunsPage() {
const [detailOpen, setDetailOpen] = useState(false)
const [detailLoading, setDetailLoading] = useState(false)
const [activeRun, setActiveRun] = useState<AIWorkflowRun | null>(null)
const [conversationOpen, setConversationOpen] = useState(false)
const [conversationLoading, setConversationLoading] = useState(false)
const [activeConversation, setActiveConversation] = useState<AdminConversationDetail | null>(null)
const [conversationMessages, setConversationMessages] = useState<AdminMessage[]>([])
const statusOptions = useMemo(() => getStatusOptions(t), [t])
const agentOptions = useMemo(
() => [
@@ -107,6 +122,27 @@ export default function DashboardAIWorkflowRunsPage() {
}
}
async function openConversation(conversationId: number) {
if (!conversationId) {
return
}
setConversationOpen(true)
setConversationLoading(true)
try {
const [detail, messagePage] = await Promise.all([
fetchConversationDetail(conversationId),
fetchConversationMessages({ conversationId, limit: 20 }),
])
setActiveConversation(detail)
setConversationMessages(messagePage.results ?? [])
} catch (error) {
toast.error(error instanceof Error ? error.message : t("workflowRun.loadConversationFailed"))
setConversationOpen(false)
} finally {
setConversationLoading(false)
}
}
return (
<>
<DashboardListPage<AIWorkflowRun>
@@ -198,7 +234,21 @@ export default function DashboardAIWorkflowRunsPage() {
className: "w-48",
render: (item) => (
<div className="space-y-1 text-sm">
<div>{t("workflowRun.conversationShort", { id: item.conversationId || "-" })}</div>
<Button
type="button"
variant="ghost"
size="sm"
className="h-auto max-w-full justify-start px-0 py-0 text-sm font-normal hover:bg-transparent hover:text-primary"
onClick={(event) => {
event.stopPropagation()
void openConversation(item.conversationId)
}}
>
<MessageCircleMoreIcon className="size-3.5" />
<span className="truncate">
{t("workflowRun.conversationShort", { id: item.conversationId || "-" })}
</span>
</Button>
<div className="text-xs text-muted-foreground">
{t("workflowRun.messageShort", { id: item.messageId || "-" })}
</div>
@@ -224,13 +274,10 @@ export default function DashboardAIWorkflowRunsPage() {
{
key: "error",
label: t("workflowRun.error"),
className: "min-w-48",
className: "w-72 max-w-72",
render: (item) =>
item.errorMessage ? (
<div className="flex items-start gap-1.5 text-xs text-destructive">
<AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
<span className="line-clamp-2 break-all">{item.errorMessage}</span>
</div>
<ErrorMessagePreview message={item.errorMessage} />
) : (
<span className="text-muted-foreground">-</span>
),
@@ -255,23 +302,83 @@ export default function DashboardAIWorkflowRunsPage() {
}
}}
t={t}
onOpenConversation={openConversation}
/>
<ConversationDetailDialog
open={conversationOpen}
loading={conversationLoading}
saving={false}
readOnly
item={activeConversation}
detail={activeConversation}
messages={conversationMessages}
messagesHasMore={false}
loadingMoreMessages={false}
onOpenChange={(open) => {
setConversationOpen(open)
if (!open) {
setActiveConversation(null)
setConversationMessages([])
}
}}
onOpenAssign={() => undefined}
onDispatch={async () => undefined}
onOpenTransfer={() => undefined}
onRead={async () => undefined}
onOpenClose={() => undefined}
/>
</>
)
}
function ErrorMessagePreview({ message }: { message: string }) {
const [open, setOpen] = useState(false)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<button
type="button"
className="flex w-full min-w-0 items-center gap-1.5 text-left text-xs text-destructive"
onClick={(event) => event.stopPropagation()}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
/>
}
>
<AlertTriangleIcon className="size-3.5 shrink-0" />
<span className="block min-w-0 truncate">{message}</span>
</PopoverTrigger>
<PopoverContent
align="end"
className="max-h-80 w-[30rem] max-w-[min(30rem,calc(100vw-2rem))] overflow-auto whitespace-pre-wrap break-words text-xs text-destructive"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onClick={(event) => event.stopPropagation()}
>
{message}
</PopoverContent>
</Popover>
)
}
function WorkflowRunDetailDialog({
open,
loading,
run,
onOpenChange,
t,
onOpenConversation,
}: {
open: boolean
loading: boolean
run: AIWorkflowRun | null
onOpenChange: (open: boolean) => void
t: TFunction
onOpenConversation: (conversationId: number) => void | Promise<void>
}) {
return (
<ProjectDialog
@@ -303,7 +410,19 @@ function WorkflowRunDetailDialog({
<DetailRow label={t("workflowRun.version")} value={`v${run.workflowVersion || "-"} / #${run.workflowVersionId}`} />
<DetailRow label={t("workflowRun.agent")} value={run.aiAgentName || `#${run.aiAgentId}`} />
<DetailRow label={t("workflowRun.status")} value={formatRunStatus(run)} />
<DetailRow label={t("workflowRun.conversationId")} value={`#${run.conversationId}`} />
<DetailRow
label={t("workflowRun.conversationId")}
value={
<Button
type="button"
variant="link"
className="h-auto min-w-0 px-0 py-0 font-medium"
onClick={() => void onOpenConversation(run.conversationId)}
>
#{run.conversationId}
</Button>
}
/>
<DetailRow label={t("workflowRun.messageId")} value={`#${run.messageId}`} />
<DetailRow label={t("workflowRun.startedAt")} value={run.startedAt ? formatDateTime(run.startedAt) : "-"} />
<DetailRow label={t("workflowRun.endedAt")} value={run.endedAt ? formatDateTime(run.endedAt) : "-"} />
@@ -396,7 +515,7 @@ function PreviewBlock({
)
}
function DetailRow({ label, value }: { label: string; value: string }) {
function DetailRow({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="flex min-w-0 items-center justify-between gap-3">
<span className="shrink-0 text-xs text-muted-foreground">{label}</span>
@@ -32,6 +32,7 @@ type ConversationDetailDialogProps = {
open: boolean;
loading: boolean;
saving: boolean;
readOnly?: boolean;
item: AdminConversation | null;
detail: AdminConversationDetail | null;
messages?: AdminMessage[] | null;
@@ -164,6 +165,7 @@ export function ConversationDetailDialog({
open,
loading,
saving,
readOnly = false,
item,
detail,
messages: rawMessages,
@@ -301,53 +303,59 @@ export function ConversationDetailDialog({
})
: t("conversationMonitor.noConversationInfo")}
</div>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={onOpenAssign}
disabled={saving || !currentConversation || currentConversation.status !== 2}
>
<MessageCircleMoreIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.assign")}
{readOnly ? (
<Button variant="outline" onClick={() => onOpenChange(false)}>
{t("common.close")}
</Button>
<Button
variant="outline"
onClick={() => void onDispatch()}
disabled={
saving || !currentConversation || !isPendingConversation
}
>
<MessageCircleMoreIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.retryDispatch")}
</Button>
<Button
variant="outline"
onClick={() => void onRead()}
disabled={saving || !currentConversation}
>
<CheckCheckIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.markRead")}
</Button>
<Button
type="button"
variant="outline"
onClick={onOpenTransfer}
disabled={saving || !currentConversation || currentConversation.status !== 3}
>
<MessageCircleMoreIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.transfer")}
</Button>
{!isClosedConversation ? (
) : (
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={onOpenClose}
onClick={onOpenAssign}
disabled={saving || !currentConversation || currentConversation.status !== 2}
>
<MessageCircleMoreIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.assign")}
</Button>
<Button
variant="outline"
onClick={() => void onDispatch()}
disabled={
saving || !currentConversation || !isPendingConversation
}
>
<MessageCircleMoreIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.retryDispatch")}
</Button>
<Button
variant="outline"
onClick={() => void onRead()}
disabled={saving || !currentConversation}
>
<EyeIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.close")}
<CheckCheckIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.markRead")}
</Button>
) : null}
</div>
<Button
type="button"
variant="outline"
onClick={onOpenTransfer}
disabled={saving || !currentConversation || currentConversation.status !== 3}
>
<MessageCircleMoreIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.transfer")}
</Button>
{!isClosedConversation ? (
<Button
variant="outline"
onClick={onOpenClose}
disabled={saving || !currentConversation}
>
<EyeIcon />
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.close")}
</Button>
) : null}
</div>
)}
</div>
}
>
+1
View File
@@ -2262,6 +2262,7 @@
"allAgents": "All agents",
"loadAgentsFailed": "Could not load AI agents.",
"loadDetailFailed": "Could not load workflow run details.",
"loadConversationFailed": "Could not load conversation details.",
"conversationId": "Conversation ID",
"messageId": "Message ID",
"workflowVersionId": "Workflow Version ID",
+1
View File
@@ -2262,6 +2262,7 @@
"allAgents": "全部 Agent",
"loadAgentsFailed": "加载 AI Agent 列表失败",
"loadDetailFailed": "加载流程执行详情失败",
"loadConversationFailed": "加载会话详情失败",
"conversationId": "会话ID",
"messageId": "消息ID",
"workflowVersionId": "流程版本ID",