From 2691164fc4b72a304b1a3123f3a1f8b5dcafc5c5 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 1 Mar 2024 18:22:09 +0800 Subject: [PATCH] workflow log list --- .../app/(appDetailLayout)/[appId]/layout.tsx | 11 +- .../(appDetailLayout)/[appId]/logs/page.tsx | 5 +- .../components/app/log-annotation/index.tsx | 28 ++-- .../components/app/workflow-log/filter.tsx | 55 +++++++ web/app/components/app/workflow-log/index.tsx | 131 ++++++++++++++++ web/app/components/app/workflow-log/list.tsx | 146 ++++++++++++++++++ .../app/workflow-log/style.module.css | 9 ++ web/i18n/en-US/app-log.ts | 8 + web/i18n/en-US/app.ts | 1 + web/i18n/pt-BR/app.ts | 1 + web/i18n/uk-UA/app.ts | 1 + web/i18n/zh-Hans/app-log.ts | 8 + web/i18n/zh-Hans/app.ts | 1 + web/models/log.ts | 47 ++++++ web/service/log.ts | 16 ++ web/types/app.ts | 2 +- 16 files changed, 455 insertions(+), 15 deletions(-) create mode 100644 web/app/components/app/workflow-log/filter.tsx create mode 100644 web/app/components/app/workflow-log/index.tsx create mode 100644 web/app/components/app/workflow-log/list.tsx create mode 100644 web/app/components/app/workflow-log/style.module.css diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx index adbe305ff4..4d645e617a 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx @@ -27,12 +27,15 @@ const AppDetailLayout: FC = (props) => { const { data: response } = useSWR(detailParams, fetchAppDetail) const appModeName = (() => { - if (response?.mode === 'chat') + if (response?.mode === 'chat' || response?.mode === 'advanced-chat') return t('app.types.chatbot') - if (response?.mode === 'agent') + if (response?.mode === 'agent-chat') return t('app.types.agent') + if (response?.mode === 'completion') + return t('app.types.completion') + return t('app.types.workflow') })() @@ -55,7 +58,9 @@ const AppDetailLayout: FC = (props) => { return (
-
{children}
+
+ {React.cloneElement(children as React.ReactElement, { appMode: response.mode })} +
) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx index d23e690dc5..b79f9fd446 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx @@ -1,16 +1,19 @@ import React from 'react' import Main from '@/app/components/app/log-annotation' import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type' +import type { AppMode } from '@/types/app' export type IProps = { params: { appId: string } + appMode: AppMode } const Logs = async ({ params: { appId }, + appMode, }: IProps) => { return ( -
+
) } diff --git a/web/app/components/app/log-annotation/index.tsx b/web/app/components/app/log-annotation/index.tsx index 08351908c5..492e63ff81 100644 --- a/web/app/components/app/log-annotation/index.tsx +++ b/web/app/components/app/log-annotation/index.tsx @@ -1,21 +1,26 @@ 'use client' import type { FC } from 'react' import React from 'react' +import cn from 'classnames' import { useTranslation } from 'react-i18next' import { useRouter } from 'next/navigation' import Log from '@/app/components/app/log' +import WorkflowLog from '@/app/components/app/workflow-log' import Annotation from '@/app/components/app/annotation' import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type' import TabSlider from '@/app/components/base/tab-slider-plain' +import type { AppMode } from '@/types/app' type Props = { pageType: PageType appId: string + appMode: AppMode } const LogAnnotation: FC = ({ pageType, appId, + appMode, }) => { const { t } = useTranslation() const router = useRouter() @@ -27,17 +32,20 @@ const LogAnnotation: FC = ({ return (
- { - router.push(`/app/${appId}/${value === PageType.log ? 'logs' : 'annotations'}`) - }} - options={options} - /> -
- {pageType === PageType.log && ()} + {appMode !== 'workflow' && ( + { + router.push(`/app/${appId}/${value === PageType.log ? 'logs' : 'annotations'}`) + }} + options={options} + /> + )} +
+ {pageType === PageType.log && appMode !== 'workflow' && ()} {pageType === PageType.annotation && ()} + {pageType === PageType.log && appMode === 'workflow' && ()}
) diff --git a/web/app/components/app/workflow-log/filter.tsx b/web/app/components/app/workflow-log/filter.tsx new file mode 100644 index 0000000000..182344afd6 --- /dev/null +++ b/web/app/components/app/workflow-log/filter.tsx @@ -0,0 +1,55 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + MagnifyingGlassIcon, +} from '@heroicons/react/24/solid' +import type { QueryParam } from './index' +import { SimpleSelect } from '@/app/components/base/select' + +type IFilterProps = { + queryParams: QueryParam + setQueryParams: (v: QueryParam) => void +} + +const Filter: FC = ({ queryParams, setQueryParams }: IFilterProps) => { + const { t } = useTranslation() + return ( +
+
+ { + setQueryParams({ ...queryParams, status: item.value as string }) + } + } + items={[{ value: 'all', name: 'All' }, + { value: 'succeeded', name: 'Success' }, + { value: 'failed', name: 'Fail' }, + { value: 'stopped', name: 'Stop' }, + ]} + /> +
+
+
+
+ { + setQueryParams({ ...queryParams, keyword: e.target.value }) + }} + /> +
+
+ ) +} + +export default Filter diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx new file mode 100644 index 0000000000..6601bd923a --- /dev/null +++ b/web/app/components/app/workflow-log/index.tsx @@ -0,0 +1,131 @@ +'use client' +import type { FC, SVGProps } from 'react' +import React, { useState } from 'react' +import useSWR from 'swr' +import { usePathname } from 'next/navigation' +import { Pagination } from 'react-headless-pagination' +import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' +import { Trans, useTranslation } from 'react-i18next' +import Link from 'next/link' +import List from './list' +import Filter from './filter' +import s from './style.module.css' +import Loading from '@/app/components/base/loading' +import { fetchWorkflowLogs } from '@/service/log' +import { fetchAppDetail } from '@/service/apps' +import { APP_PAGE_LIMIT } from '@/config' +import type { AppMode } from '@/types/app' + +export type ILogsProps = { + appId: string +} + +export type QueryParam = { + status?: string + keyword?: string +} + +const ThreeDotsIcon = ({ className }: SVGProps) => { + return + + +} + +const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => { + const { t } = useTranslation() + const pathname = usePathname() + const pathSegments = pathname.split('/') + pathSegments.pop() + return
+
+ {t('appLog.table.empty.element.title')} +
+ , testLink: }} + /> +
+
+
+} + +const Logs: FC = ({ appId }) => { + const { t } = useTranslation() + const [queryParams, setQueryParams] = useState({ status: 'all' }) + const [currPage, setCurrPage] = React.useState(0) + + const query = { + page: currPage + 1, + limit: APP_PAGE_LIMIT, + ...queryParams, + } + + // Get the app type first + const { data: appDetail } = useSWR({ url: '/apps', id: appId }, fetchAppDetail) + + const getWebAppType = (appType?: AppMode) => { + if (!appType) + return '' + if (appType === 'completion' || appType === 'workflow') + return 'completion' + return 'chat' + } + + const { data: workflowLogs, mutate } = useSWR({ + url: `/apps/${appId}/workflow-app-logs`, + params: query, + }, fetchWorkflowLogs) + const total = workflowLogs?.total + + return ( +
+

{t('appLog.workflowTitle')}

+

{t('appLog.workflowSubtitle')}

+
+ + {/* workflow log */} + {total === undefined + ? + : total > 0 + ? + : + } + {/* Show Pagination only if the total is more than the limit */} + {(total && total > APP_PAGE_LIMIT) + ? + + + {t('appLog.table.pagination.previous')} + +
+ +
+ + {t('appLog.table.pagination.next')} + + +
+ : null} +
+
+ ) +} + +export default Logs diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx new file mode 100644 index 0000000000..6a089fcb68 --- /dev/null +++ b/web/app/components/app/workflow-log/list.tsx @@ -0,0 +1,146 @@ +'use client' +import type { FC } from 'react' +import React, { useState } from 'react' +import dayjs from 'dayjs' +import { createContext } from 'use-context-selector' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import s from './style.module.css' +import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log' +import type { App } from '@/types/app' +import Loading from '@/app/components/base/loading' +import Drawer from '@/app/components/base/drawer' +import Indicator from '@/app/components/header/indicator' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' + +type ILogs = { + logs?: WorkflowLogsResponse + appDetail?: App + onRefresh: () => void +} + +const defaultValue = 'N/A' + +type IDrawerContext = { + onClose: () => void + appDetail?: App +} + +const DrawerContext = createContext({} as IDrawerContext) + +const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { + const { t } = useTranslation() + + const media = useBreakpoints() + const isMobile = media === MediaType.mobile + + const [showDrawer, setShowDrawer] = useState(false) + const [currentLog, setCurrentLog] = useState() + + const statusTdRender = (status: string) => { + if (status === 'succeeded') { + return ( +
+ + Success +
+ ) + } + if (status === 'failed') { + return ( +
+ + Fail +
+ ) + } + if (status === 'stopped') { + return ( +
+ + Stop +
+ ) + } + if (status === 'running') { + return ( +
+ + Running +
+ ) + } + } + + const onCloseDrawer = () => { + onRefresh() + setShowDrawer(false) + setCurrentLog(undefined) + } + + if (!logs) + return + + return ( +
+ + + + + + + + + + {/* */} + + + + {logs.data.map((log: WorkflowAppLogDetail) => { + const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : defaultValue + return { + setShowDrawer(true) + setCurrentLog(log) + }}> + + + + + + + {/* */} + + })} + +
{t('appLog.table.header.startTime')}{t('appLog.table.header.status')}{t('appLog.table.header.runtime')}{t('appLog.table.header.tokens')}{t('appLog.table.header.user')}{t('appLog.table.header.version')}
{!log.read_at && }{dayjs.unix(log.created_at).format(t('appLog.dateTimeFormat') as string)}{statusTdRender(log.workflow_run.status)} +
10 && 'text-orange-400', + )}>{`${log.workflow_run.elapsed_time}s`}
+
{log.workflow_run.total_tokens} +
+ {endUser} +
+
VERSION
+ + + {
TODO
} +
+
+
+ ) +} + +export default WorkflowAppLogList diff --git a/web/app/components/app/workflow-log/style.module.css b/web/app/components/app/workflow-log/style.module.css new file mode 100644 index 0000000000..67a9fe3bf5 --- /dev/null +++ b/web/app/components/app/workflow-log/style.module.css @@ -0,0 +1,9 @@ +.logTable td { + padding: 7px 8px; + box-sizing: border-box; + max-width: 200px; +} + +.pagination li { + list-style: none; +} diff --git a/web/i18n/en-US/app-log.ts b/web/i18n/en-US/app-log.ts index 4620ce8be4..9a0d4f419f 100644 --- a/web/i18n/en-US/app-log.ts +++ b/web/i18n/en-US/app-log.ts @@ -12,6 +12,12 @@ const translation = { messageCount: 'Message Count', userRate: 'User Rate', adminRate: 'Op. Rate', + startTime: 'START TIME', + status: 'STATUS', + runtime: 'RUN TIME', + tokens: 'TOKENS', + user: 'END-USER', + version: 'VERSION', }, pagination: { previous: 'Prev', @@ -64,6 +70,8 @@ const translation = { not_annotated: 'Not Annotated', }, }, + workflowTitle: 'Workflow Logs', + workflowSubtitle: 'The log recorded the operation of Automate.', } export default translation diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index f8de26d4e5..d5fda730c0 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -5,6 +5,7 @@ const translation = { chatbot: 'Chatbot', agent: 'Agent', workflow: 'Workflow', + completion: 'Completion', }, duplicate: 'Duplicate', duplicateTitle: 'Duplicate App', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 1632e61307..471ba4874d 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -5,6 +5,7 @@ const translation = { chatbot: 'Chatbot', agent: 'Agente', workflow: 'Fluxo de trabalho', + completion: 'Gerador de Texto', }, duplicate: 'Duplicar', duplicateTitle: 'Duplicate aplicativo', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 8f06faecd8..7bf95f1232 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -5,6 +5,7 @@ const translation = { // Add the Ukrainian translation object chatbot: 'Чатбот', agent: 'Агент', workflow: 'Робочий Процес', + completion: 'Автодоповнення', }, duplicate: 'Дублювати', duplicateTitle: 'Дублювати додаток', diff --git a/web/i18n/zh-Hans/app-log.ts b/web/i18n/zh-Hans/app-log.ts index c038bae3e5..6355ccba87 100644 --- a/web/i18n/zh-Hans/app-log.ts +++ b/web/i18n/zh-Hans/app-log.ts @@ -12,6 +12,12 @@ const translation = { messageCount: '消息数', userRate: '用户反馈', adminRate: '管理员反馈', + startTime: '开始时间', + status: '状态', + runtime: '运行时间', + tokens: 'TOKENS', + user: '用户', + version: '版本', }, pagination: { previous: '上一页', @@ -64,6 +70,8 @@ const translation = { not_annotated: '未标注', }, }, + workflowTitle: 'Workflow Logs', + workflowSubtitle: 'The log recorded the operation of Automate.', } export default translation diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 7081bed545..d074b82414 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -5,6 +5,7 @@ const translation = { chatbot: '聊天助手', agent: 'Agent', workflow: '工作流', + completion: '文本生成', }, duplicate: '复制', duplicateTitle: '复制应用', diff --git a/web/models/log.ts b/web/models/log.ts index 20a527165a..471bfa7e1f 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -217,3 +217,50 @@ export type LogMessageAnnotationsResponse = LogMessageFeedbacksResponse export type AnnotationsCountResponse = { count: number } + +export type WorkflowRunDetail = { + id: string + version: string + status: 'running' | 'succeeded' | 'failed' | 'stopped' + error?: string + elapsed_time: number + total_tokens: number + total_price: number + currency: string + total_steps: number + finished_at: number +} +export type AccountInfo = { + id: string + name: string + email: string +} +export type EndUserInfo = { + id: string + type: 'browser' | 'service_api' + is_anonymous: boolean + session_id: string +} +export type WorkflowAppLogDetail = { + id: string + workflow_run: WorkflowRunDetail + created_from: 'service-api' | 'web-app' | 'explore' + created_by_role: 'account' | 'end_user' + created_by_account?: AccountInfo + created_by_end_user?: EndUserInfo + created_at: number + read_at?: number +} +export type WorkflowLogsResponse = { + data: Array + has_more: boolean + limit: number + total: number + page: number +} +export type WorkflowLogsRequest = { + keyword: string + status: string + page: number + limit: number // The default value is 20 and the range is 1-100 +} diff --git a/web/service/log.ts b/web/service/log.ts index 0280874c67..b7fce991ef 100644 --- a/web/service/log.ts +++ b/web/service/log.ts @@ -15,6 +15,8 @@ import type { LogMessageAnnotationsResponse, LogMessageFeedbacksRequest, LogMessageFeedbacksResponse, + WorkflowLogsRequest, + WorkflowLogsResponse, } from '@/models/log' export const fetchConversationList: Fetcher }> = ({ appId, params }) => { @@ -57,3 +59,17 @@ export const updateLogMessageAnnotations: Fetcher = ({ url }) => { return get(url) } + +export const fetchWorkflowLogs: Fetcher = ({ url, params }) => { + return get(url, { params }) +} + +// TODO +export const fetchFullRunDetail: Fetcher = ({ url, params }) => { + return get(url, { params }) +} + +// TODO +export const fetchTracingDetail: Fetcher = ({ url, params }) => { + return get(url, { params }) +} diff --git a/web/types/app.ts b/web/types/app.ts index 5b059703b3..7d5ff08eca 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -44,7 +44,7 @@ export type VariableInput = { /** * App modes */ -export const AppModes = ['agent', 'chat', 'workflow'] as const +export const AppModes = ['advanced-chat', 'agent-chat', 'chat', 'completion', 'workflow'] as const export type AppMode = typeof AppModes[number] /**