mirror of
https://github.com/langgenius/dify.git
synced 2026-08-01 18:30:48 +08:00
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
import type { AgentLogConversationItemResponse } from '@dify/contracts/api/console/agent/types.gen'
|
|
import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import {
|
|
ScrollAreaContent,
|
|
ScrollAreaRoot,
|
|
ScrollAreaScrollbar,
|
|
ScrollAreaThumb,
|
|
ScrollAreaViewport,
|
|
} from '@langgenius/dify-ui/scroll-area'
|
|
import { useTranslation } from 'react-i18next'
|
|
import useTimestamp from '@/hooks/use-timestamp'
|
|
import { LogSourceCell } from './source-cell'
|
|
|
|
export function AgentLogsTable({
|
|
logs,
|
|
isPending,
|
|
isError,
|
|
isSuccess,
|
|
selectedLogId,
|
|
onOpenLog,
|
|
onRetry,
|
|
}: {
|
|
logs: AgentLogConversationItemResponse[]
|
|
isPending: boolean
|
|
isError: boolean
|
|
isSuccess: boolean
|
|
selectedLogId?: string
|
|
onOpenLog: (log: AgentLogConversationItemResponse) => void
|
|
onRetry: () => void
|
|
}) {
|
|
const { t } = useTranslation('agentV2')
|
|
const tableHeaderLabels = {
|
|
unread: t(($) => $['agentDetail.logs.table.unread']),
|
|
title: t(($) => $['agentDetail.logs.table.title']),
|
|
source: t(($) => $['agentDetail.logs.table.source']),
|
|
endUser: t(($) => $['agentDetail.logs.table.endUser']),
|
|
messageCount: t(($) => $['agentDetail.logs.table.messageCount']),
|
|
userRate: t(($) => $['agentDetail.logs.table.userRate']),
|
|
operationRate: t(($) => $['agentDetail.logs.table.operationRate']),
|
|
updatedTime: t(($) => $['agentDetail.logs.table.updatedTime']),
|
|
createdTime: t(($) => $['agentDetail.logs.table.createdTime']),
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full min-w-0 flex-col overflow-hidden">
|
|
<div className="shrink-0 pr-3">
|
|
<table aria-hidden="true" className="w-full table-fixed border-collapse">
|
|
<LogsTableColGroup />
|
|
<LogsTableHeader labels={tableHeaderLabels} />
|
|
</table>
|
|
</div>
|
|
|
|
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
|
|
<ScrollAreaViewport
|
|
aria-label={t(($) => $['agentDetail.logs.title'])}
|
|
role="region"
|
|
tabIndex={-1}
|
|
className="overscroll-contain"
|
|
>
|
|
<ScrollAreaContent className="pr-3">
|
|
<table className="w-full table-fixed border-collapse">
|
|
<LogsTableColGroup />
|
|
<LogsTableHeader labels={tableHeaderLabels} rowClassName="sr-only" />
|
|
<AgentLogsTableBody
|
|
logs={logs}
|
|
isPending={isPending}
|
|
isError={isError}
|
|
isSuccess={isSuccess}
|
|
selectedLogId={selectedLogId}
|
|
onOpenLog={onOpenLog}
|
|
onRetry={onRetry}
|
|
/>
|
|
</table>
|
|
</ScrollAreaContent>
|
|
</ScrollAreaViewport>
|
|
<ScrollAreaScrollbar>
|
|
<ScrollAreaThumb />
|
|
</ScrollAreaScrollbar>
|
|
</ScrollAreaRoot>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AgentLogsTableBody({
|
|
logs,
|
|
isPending,
|
|
isError,
|
|
isSuccess,
|
|
selectedLogId,
|
|
onOpenLog,
|
|
onRetry,
|
|
}: {
|
|
logs: AgentLogConversationItemResponse[]
|
|
isPending: boolean
|
|
isError: boolean
|
|
isSuccess: boolean
|
|
selectedLogId?: string
|
|
onOpenLog: (log: AgentLogConversationItemResponse) => void
|
|
onRetry: () => void
|
|
}) {
|
|
const { t } = useTranslation('agentV2')
|
|
const { t: tCommon } = useTranslation('common')
|
|
const { formatTime } = useTimestamp()
|
|
const notAvailable = t(($) => $['agentDetail.logs.notAvailable'])
|
|
const formatLogTime = (value?: number | null) =>
|
|
value == null
|
|
? notAvailable
|
|
: formatTime(
|
|
value,
|
|
t(($) => $['roster.dateTimeFormat']),
|
|
)
|
|
|
|
return (
|
|
<tbody className="system-sm-regular text-text-secondary">
|
|
{isPending && <LogsSkeletonRows />}
|
|
{isError && (
|
|
<LogsStateRow>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<span>{t(($) => $['agentDetail.logs.loadFailed'])}</span>
|
|
<Button variant="secondary" size="small" onClick={onRetry}>
|
|
{tCommon(($) => $['operation.retry'])}
|
|
</Button>
|
|
</div>
|
|
</LogsStateRow>
|
|
)}
|
|
{isSuccess && logs.length === 0 && (
|
|
<LogsStateRow>{t(($) => $['agentDetail.logs.empty'])}</LogsStateRow>
|
|
)}
|
|
{isSuccess &&
|
|
logs.map((log) => {
|
|
const logTitle = log.title || log.conversation_id
|
|
const isSelected = selectedLogId === log.id
|
|
|
|
return (
|
|
<tr
|
|
key={log.id}
|
|
className={cn(
|
|
'h-10 cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover',
|
|
isSelected && 'bg-background-default-hover',
|
|
)}
|
|
onClick={() => onOpenLog(log)}
|
|
>
|
|
<td className="px-0">
|
|
<span
|
|
className={cn(
|
|
'mx-auto block size-1.5 rounded-full',
|
|
log.unread ? 'bg-util-colors-blue-blue-500' : 'bg-transparent',
|
|
)}
|
|
/>
|
|
</td>
|
|
<TableCell>
|
|
<button
|
|
type="button"
|
|
aria-label={logTitle}
|
|
className="block w-full truncate rounded-sm text-left system-sm-medium text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
onOpenLog(log)
|
|
}}
|
|
>
|
|
{log.title || notAvailable}
|
|
</button>
|
|
</TableCell>
|
|
<td className="px-3">
|
|
<LogSourceCell source={log.source} />
|
|
</td>
|
|
<TableCell translate="no">{log.end_user_id || notAvailable}</TableCell>
|
|
<TableCell className="tabular-nums">{log.message_count}</TableCell>
|
|
<TableCell className="text-text-quaternary">
|
|
{formatRate(log.user_rate, notAvailable)}
|
|
</TableCell>
|
|
<TableCell className="text-text-quaternary">
|
|
{formatRate(log.operation_rate, notAvailable)}
|
|
</TableCell>
|
|
<TableCell>{formatLogTime(log.updated_at)}</TableCell>
|
|
<TableCell>{formatLogTime(log.created_at)}</TableCell>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
)
|
|
}
|
|
|
|
function formatRate(value: number | null | undefined, fallback: string) {
|
|
return value == null ? fallback : `${Math.round(value * 100)}%`
|
|
}
|
|
|
|
type LogsTableHeaderLabels = {
|
|
unread: string
|
|
title: string
|
|
source: string
|
|
endUser: string
|
|
messageCount: string
|
|
userRate: string
|
|
operationRate: string
|
|
updatedTime: string
|
|
createdTime: string
|
|
}
|
|
|
|
function LogsTableHeader({
|
|
labels,
|
|
rowClassName,
|
|
}: {
|
|
labels: LogsTableHeaderLabels
|
|
rowClassName?: string
|
|
}) {
|
|
return (
|
|
<thead>
|
|
<tr
|
|
className={cn(
|
|
'h-7 bg-background-section-burn text-left system-xs-medium-uppercase text-text-tertiary',
|
|
rowClassName,
|
|
)}
|
|
>
|
|
<th scope="col" className="rounded-l-lg px-0">
|
|
<span className="sr-only">{labels.unread}</span>
|
|
</th>
|
|
<TableHead>{labels.title}</TableHead>
|
|
<TableHead>{labels.source}</TableHead>
|
|
<TableHead>{labels.endUser}</TableHead>
|
|
<TableHead>{labels.messageCount}</TableHead>
|
|
<TableHead>{labels.userRate}</TableHead>
|
|
<TableHead>{labels.operationRate}</TableHead>
|
|
<TableHead>{labels.updatedTime}</TableHead>
|
|
<TableHead className="rounded-r-lg">{labels.createdTime}</TableHead>
|
|
</tr>
|
|
</thead>
|
|
)
|
|
}
|
|
|
|
function LogsTableColGroup() {
|
|
return (
|
|
<colgroup>
|
|
<col className="w-5" />
|
|
<col className="w-[28%]" />
|
|
<col className="w-[16%]" />
|
|
<col className="w-[15%]" />
|
|
<col className="w-[8%]" />
|
|
<col className="w-[7%]" />
|
|
<col className="w-[7%]" />
|
|
<col className="w-[10%]" />
|
|
<col className="w-[9%]" />
|
|
</colgroup>
|
|
)
|
|
}
|
|
|
|
function LogsStateRow({ children }: { children: ReactNode }) {
|
|
return (
|
|
<tr className="h-20 border-b border-divider-subtle">
|
|
<td colSpan={9} className="px-3 text-center text-text-tertiary">
|
|
{children}
|
|
</td>
|
|
</tr>
|
|
)
|
|
}
|
|
|
|
function LogsSkeletonRows() {
|
|
return (
|
|
<>
|
|
{Array.from({ length: 10 }, (_, index) => (
|
|
<tr key={index} className="h-10 border-b border-divider-subtle">
|
|
<td className="px-0">
|
|
<span className="mx-auto block size-1.5 rounded-full bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-3/4 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="flex items-center gap-2">
|
|
<div className="size-5 rounded-full bg-text-quaternary opacity-20" />
|
|
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
|
|
</div>
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-28 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
<td className="px-3">
|
|
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function TableHead({ className, ...props }: ThHTMLAttributes<HTMLTableCellElement>) {
|
|
return <th scope="col" className={cn('px-3 text-left whitespace-nowrap', className)} {...props} />
|
|
}
|
|
|
|
function TableCell({ children, className, ...props }: TdHTMLAttributes<HTMLTableCellElement>) {
|
|
return (
|
|
<td className={cn('min-w-0 px-3 whitespace-nowrap', className)} {...props}>
|
|
<div className="truncate">{children}</div>
|
|
</td>
|
|
)
|
|
}
|