fix(workflow): improve run audit interactions
This commit is contained in:
@@ -1,23 +1,34 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState, type ReactNode } from "react"
|
||||||
import {
|
import {
|
||||||
AlertTriangleIcon,
|
AlertTriangleIcon,
|
||||||
Clock3Icon,
|
Clock3Icon,
|
||||||
|
MessageCircleMoreIcon,
|
||||||
MessageSquareTextIcon,
|
MessageSquareTextIcon,
|
||||||
WorkflowIcon,
|
WorkflowIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
import { ConversationDetailDialog } from "@/app/dashboard/conversation-monitor/_components/detail"
|
||||||
import { DashboardListPage } from "@/components/dashboard/list"
|
import { DashboardListPage } from "@/components/dashboard/list"
|
||||||
import { JsonTreeViewer } from "@/components/json-tree-viewer"
|
import { JsonTreeViewer } from "@/components/json-tree-viewer"
|
||||||
import { ProjectDialog } from "@/components/project-dialog"
|
import { ProjectDialog } from "@/components/project-dialog"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
import {
|
import {
|
||||||
fetchAIAgentsAll,
|
fetchAIAgentsAll,
|
||||||
fetchAIWorkflowRun,
|
fetchAIWorkflowRun,
|
||||||
fetchAIWorkflowRuns,
|
fetchAIWorkflowRuns,
|
||||||
|
fetchConversationDetail,
|
||||||
|
fetchConversationMessages,
|
||||||
|
type AdminConversationDetail,
|
||||||
|
type AdminMessage,
|
||||||
type AIAgent,
|
type AIAgent,
|
||||||
type AIWorkflowNodeRun,
|
type AIWorkflowNodeRun,
|
||||||
type AIWorkflowRun,
|
type AIWorkflowRun,
|
||||||
@@ -59,6 +70,10 @@ export default function DashboardAIWorkflowRunsPage() {
|
|||||||
const [detailOpen, setDetailOpen] = useState(false)
|
const [detailOpen, setDetailOpen] = useState(false)
|
||||||
const [detailLoading, setDetailLoading] = useState(false)
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
const [activeRun, setActiveRun] = useState<AIWorkflowRun | null>(null)
|
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 statusOptions = useMemo(() => getStatusOptions(t), [t])
|
||||||
const agentOptions = useMemo(
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DashboardListPage<AIWorkflowRun>
|
<DashboardListPage<AIWorkflowRun>
|
||||||
@@ -198,7 +234,21 @@ export default function DashboardAIWorkflowRunsPage() {
|
|||||||
className: "w-48",
|
className: "w-48",
|
||||||
render: (item) => (
|
render: (item) => (
|
||||||
<div className="space-y-1 text-sm">
|
<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">
|
<div className="text-xs text-muted-foreground">
|
||||||
{t("workflowRun.messageShort", { id: item.messageId || "-" })}
|
{t("workflowRun.messageShort", { id: item.messageId || "-" })}
|
||||||
</div>
|
</div>
|
||||||
@@ -224,13 +274,10 @@ export default function DashboardAIWorkflowRunsPage() {
|
|||||||
{
|
{
|
||||||
key: "error",
|
key: "error",
|
||||||
label: t("workflowRun.error"),
|
label: t("workflowRun.error"),
|
||||||
className: "min-w-48",
|
className: "w-72 max-w-72",
|
||||||
render: (item) =>
|
render: (item) =>
|
||||||
item.errorMessage ? (
|
item.errorMessage ? (
|
||||||
<div className="flex items-start gap-1.5 text-xs text-destructive">
|
<ErrorMessagePreview message={item.errorMessage} />
|
||||||
<AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
|
|
||||||
<span className="line-clamp-2 break-all">{item.errorMessage}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">-</span>
|
<span className="text-muted-foreground">-</span>
|
||||||
),
|
),
|
||||||
@@ -255,23 +302,83 @@ export default function DashboardAIWorkflowRunsPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
t={t}
|
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({
|
function WorkflowRunDetailDialog({
|
||||||
open,
|
open,
|
||||||
loading,
|
loading,
|
||||||
run,
|
run,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
t,
|
t,
|
||||||
|
onOpenConversation,
|
||||||
}: {
|
}: {
|
||||||
open: boolean
|
open: boolean
|
||||||
loading: boolean
|
loading: boolean
|
||||||
run: AIWorkflowRun | null
|
run: AIWorkflowRun | null
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
t: TFunction
|
t: TFunction
|
||||||
|
onOpenConversation: (conversationId: number) => void | Promise<void>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ProjectDialog
|
<ProjectDialog
|
||||||
@@ -303,7 +410,19 @@ function WorkflowRunDetailDialog({
|
|||||||
<DetailRow label={t("workflowRun.version")} value={`v${run.workflowVersion || "-"} / #${run.workflowVersionId}`} />
|
<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.agent")} value={run.aiAgentName || `#${run.aiAgentId}`} />
|
||||||
<DetailRow label={t("workflowRun.status")} value={formatRunStatus(run)} />
|
<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.messageId")} value={`#${run.messageId}`} />
|
||||||
<DetailRow label={t("workflowRun.startedAt")} value={run.startedAt ? formatDateTime(run.startedAt) : "-"} />
|
<DetailRow label={t("workflowRun.startedAt")} value={run.startedAt ? formatDateTime(run.startedAt) : "-"} />
|
||||||
<DetailRow label={t("workflowRun.endedAt")} value={run.endedAt ? formatDateTime(run.endedAt) : "-"} />
|
<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 (
|
return (
|
||||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">{label}</span>
|
<span className="shrink-0 text-xs text-muted-foreground">{label}</span>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type ConversationDetailDialogProps = {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
item: AdminConversation | null;
|
item: AdminConversation | null;
|
||||||
detail: AdminConversationDetail | null;
|
detail: AdminConversationDetail | null;
|
||||||
messages?: AdminMessage[] | null;
|
messages?: AdminMessage[] | null;
|
||||||
@@ -164,6 +165,7 @@ export function ConversationDetailDialog({
|
|||||||
open,
|
open,
|
||||||
loading,
|
loading,
|
||||||
saving,
|
saving,
|
||||||
|
readOnly = false,
|
||||||
item,
|
item,
|
||||||
detail,
|
detail,
|
||||||
messages: rawMessages,
|
messages: rawMessages,
|
||||||
@@ -301,53 +303,59 @@ export function ConversationDetailDialog({
|
|||||||
})
|
})
|
||||||
: t("conversationMonitor.noConversationInfo")}
|
: t("conversationMonitor.noConversationInfo")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
{readOnly ? (
|
||||||
<Button
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
variant="outline"
|
{t("common.close")}
|
||||||
onClick={onOpenAssign}
|
|
||||||
disabled={saving || !currentConversation || currentConversation.status !== 2}
|
|
||||||
>
|
|
||||||
<MessageCircleMoreIcon />
|
|
||||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.assign")}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
) : (
|
||||||
variant="outline"
|
<div className="flex flex-wrap gap-2">
|
||||||
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 ? (
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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}
|
disabled={saving || !currentConversation}
|
||||||
>
|
>
|
||||||
<EyeIcon />
|
<CheckCheckIcon />
|
||||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.close")}
|
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.markRead")}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
<Button
|
||||||
</div>
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2262,6 +2262,7 @@
|
|||||||
"allAgents": "All agents",
|
"allAgents": "All agents",
|
||||||
"loadAgentsFailed": "Could not load AI agents.",
|
"loadAgentsFailed": "Could not load AI agents.",
|
||||||
"loadDetailFailed": "Could not load workflow run details.",
|
"loadDetailFailed": "Could not load workflow run details.",
|
||||||
|
"loadConversationFailed": "Could not load conversation details.",
|
||||||
"conversationId": "Conversation ID",
|
"conversationId": "Conversation ID",
|
||||||
"messageId": "Message ID",
|
"messageId": "Message ID",
|
||||||
"workflowVersionId": "Workflow Version ID",
|
"workflowVersionId": "Workflow Version ID",
|
||||||
|
|||||||
@@ -2262,6 +2262,7 @@
|
|||||||
"allAgents": "全部 Agent",
|
"allAgents": "全部 Agent",
|
||||||
"loadAgentsFailed": "加载 AI Agent 列表失败",
|
"loadAgentsFailed": "加载 AI Agent 列表失败",
|
||||||
"loadDetailFailed": "加载流程执行详情失败",
|
"loadDetailFailed": "加载流程执行详情失败",
|
||||||
|
"loadConversationFailed": "加载会话详情失败",
|
||||||
"conversationId": "会话ID",
|
"conversationId": "会话ID",
|
||||||
"messageId": "消息ID",
|
"messageId": "消息ID",
|
||||||
"workflowVersionId": "流程版本ID",
|
"workflowVersionId": "流程版本ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user