Browse Source

Feat: frontend support timezone of timestamp (#4070)

KVOJJJin 11 months ago
parent
commit
c0476c7881

+ 5 - 4
web/app/components/app/annotation/edit-annotation-modal/index.tsx

@@ -2,7 +2,6 @@
 import type { FC } from 'react'
 import React, { useState } from 'react'
 import { useTranslation } from 'react-i18next'
-import dayjs from 'dayjs'
 import EditItem, { EditItemType } from './edit-item'
 import Drawer from '@/app/components/base/drawer-plus'
 import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
@@ -11,6 +10,8 @@ import { addAnnotation, editAnnotation } from '@/service/annotation'
 import Toast from '@/app/components/base/toast'
 import { useProviderContext } from '@/context/provider-context'
 import AnnotationFull from '@/app/components/billing/annotation-full'
+import useTimestamp from '@/hooks/use-timestamp'
+
 type Props = {
   isShow: boolean
   onHide: () => void
@@ -41,6 +42,7 @@ const EditAnnotationModal: FC<Props> = ({
   onlyEditResponse,
 }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const { plan, enableBilling } = useProviderContext()
   const isAdd = !annotationId
   const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
@@ -117,15 +119,14 @@ const EditAnnotationModal: FC<Props> = ({
                       <MessageCheckRemove />
                       <div>{t('appAnnotation.editModal.removeThisCache')}</div>
                     </div>
-                    {createdAt && <div>{t('appAnnotation.editModal.createdAt')}&nbsp;{dayjs(createdAt * 1000).format('YYYY-MM-DD HH:mm')}</div>}
+                    {createdAt && <div>{t('appAnnotation.editModal.createdAt')}&nbsp;{formatTime(createdAt, t('appLog.dateTimeFormat') as string)}</div>}
                   </div>
                 )
                 : undefined
             }
           </div>
         }
-      >
-      </Drawer>
+      />
       <DeleteConfirmModal
         isShow={showModal}
         onHide={() => setShowModal(false)}

+ 3 - 2
web/app/components/app/annotation/list.tsx

@@ -3,11 +3,11 @@ import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
-import dayjs from 'dayjs'
 import { Edit02, Trash03 } from '../../base/icons/src/vender/line/general'
 import s from './style.module.css'
 import type { AnnotationItem } from './type'
 import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type Props = {
   list: AnnotationItem[]
@@ -21,6 +21,7 @@ const List: FC<Props> = ({
   onRemove,
 }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const [currId, setCurrId] = React.useState<string | null>(null)
   const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
   return (
@@ -54,7 +55,7 @@ const List: FC<Props> = ({
                 className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
                 title={item.answer}
               >{item.answer}</td>
-              <td>{dayjs(item.created_at * 1000).format('YYYY-MM-DD HH:mm')}</td>
+              <td>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
               <td>{item.hit_count}</td>
               <td className='w-[96px]' onClick={e => e.stopPropagation()}>
                 {/* Actions */}

+ 6 - 6
web/app/components/app/annotation/view-annotation-modal/index.tsx

@@ -3,7 +3,6 @@ import type { FC } from 'react'
 import React, { useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
-import dayjs from 'dayjs'
 import { Pagination } from 'react-headless-pagination'
 import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
 import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item'
@@ -16,6 +15,7 @@ import DeleteConfirmModal from '@/app/components/base/modal/delete-confirm-modal
 import TabSlider from '@/app/components/base/tab-slider-plain'
 import { fetchHitHistoryList } from '@/service/annotation'
 import { APP_PAGE_LIMIT } from '@/config'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type Props = {
   appId: string
@@ -43,6 +43,7 @@ const ViewAnnotationModal: FC<Props> = ({
   const [newQuestion, setNewQuery] = useState(question)
   const [newAnswer, setNewAnswer] = useState(answer)
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const [currPage, setCurrPage] = React.useState<number>(0)
   const [total, setTotal] = useState(0)
   const [hitHistoryList, setHitHistoryList] = useState<HitHistoryItem[]>([])
@@ -119,7 +120,7 @@ const ViewAnnotationModal: FC<Props> = ({
               <td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.response')}</td>
               <td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.source')}</td>
               <td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.score')}</td>
-              <td className='whitespace-nowrap w-[140px]'>{t('appAnnotation.hitHistoryTable.time')}</td>
+              <td className='whitespace-nowrap w-[160px]'>{t('appAnnotation.hitHistoryTable.time')}</td>
             </tr>
           </thead>
           <tbody className="text-gray-500">
@@ -142,7 +143,7 @@ const ViewAnnotationModal: FC<Props> = ({
                 >{item.response}</td>
                 <td>{item.source}</td>
                 <td>{item.score ? item.score.toFixed(2) : '-'}</td>
-                <td>{dayjs(item.created_at * 1000).format('YYYY-MM-DD HH:mm')}</td>
+                <td>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
               </tr>
             ))}
           </tbody>
@@ -214,12 +215,11 @@ const ViewAnnotationModal: FC<Props> = ({
                 <MessageCheckRemove />
                 <div>{t('appAnnotation.editModal.removeThisCache')}</div>
               </div>
-              <div>{t('appAnnotation.editModal.createdAt')}&nbsp;{dayjs(createdAt * 1000).format('YYYY-MM-DD HH:mm')}</div>
+              <div>{t('appAnnotation.editModal.createdAt')}&nbsp;{formatTime(createdAt, t('appLog.dateTimeFormat') as string)}</div>
             </div>
           )
           : undefined}
-      >
-      </Drawer>
+      />
       <DeleteConfirmModal
         isShow={showModal}
         onHide={() => setShowModal(false)}

+ 16 - 6
web/app/components/app/log/list.tsx

@@ -11,6 +11,8 @@ import {
 import { get } from 'lodash-es'
 import InfiniteScroll from 'react-infinite-scroll-component'
 import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+import timezone from 'dayjs/plugin/timezone'
 import { createContext, useContext } from 'use-context-selector'
 import { useShallow } from 'zustand/react/shallow'
 import { useTranslation } from 'react-i18next'
@@ -40,6 +42,11 @@ import AgentLogModal from '@/app/components/base/agent-log-modal'
 import PromptLogModal from '@/app/components/base/prompt-log-modal'
 import MessageLogModal from '@/app/components/base/message-log-modal'
 import { useStore as useAppStore } from '@/app/components/app/store'
+import { useAppContext } from '@/context/app-context'
+import useTimestamp from '@/hooks/use-timestamp'
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
 
 type IConversationList = {
   logs?: ChatConversationsResponse | CompletionConversationsResponse
@@ -78,7 +85,7 @@ const PARAM_MAP = {
 }
 
 // Format interface data for easy display
-const getFormattedChatList = (messages: ChatMessage[], conversationId: string) => {
+const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
   const newChatList: IChatItem[] = []
   messages.forEach((item: ChatMessage) => {
     newChatList.push({
@@ -115,7 +122,7 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string) =
         query: item.query,
       },
       more: {
-        time: dayjs.unix(item.created_at).format('hh:mm A'),
+        time: dayjs.unix(item.created_at).tz(timezone).format(format),
         tokens: item.answer_tokens + item.message_tokens,
         latency: item.provider_response_latency.toFixed(2),
       },
@@ -154,6 +161,8 @@ type IDetailPanel<T> = {
 }
 
 function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
+  const { userProfile: { timezone } } = useAppContext()
+  const { formatTime } = useTimestamp()
   const { onClose, appDetail } = useContext(DrawerContext)
   const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal, showMessageLogModal, setShowMessageLogModal } = useAppStore(useShallow(state => ({
     currentLogItem: state.currentLogItem,
@@ -188,7 +197,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
         const varValues = messageRes.data[0].inputs
         setVarValues(varValues)
       }
-      const newItems = [...getFormattedChatList(messageRes.data, detail.id), ...items]
+      const newItems = [...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string), ...items]
       if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
         newItems.unshift({
           id: 'introduction',
@@ -271,7 +280,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
       <div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
         <div>
           <div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
-          <div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
+          <div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
         </div>
         <div className='flex items-center flex-wrap gap-y-1 justify-end'>
           {!isAdvanced && (
@@ -535,6 +544,7 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
  */
 const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
 
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
@@ -549,7 +559,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
       <Tooltip
         htmlContent={
           <span className='text-xs text-gray-500 inline-flex items-center'>
-            <EditIconSolid className='mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${dayjs.unix(annotation?.created_at || dayjs().unix()).format('MM-DD hh:mm A')}`}
+            <EditIconSolid className='mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
           </span>
         }
         className={(isHighlight && !isChatMode) ? '' : '!hidden'}
@@ -598,7 +608,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
                 setCurrentConversation(log)
               }}>
               <td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
-              <td className='w-[160px]'>{dayjs.unix(log.created_at).format(t('appLog.dateTimeFormat') as string)}</td>
+              <td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
               <td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
               <td style={{ maxWidth: isChatMode ? 300 : 200 }}>
                 {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}

+ 3 - 2
web/app/components/app/workflow-log/list.tsx

@@ -1,7 +1,6 @@
 'use client'
 import type { FC } from 'react'
 import React, { useState } from 'react'
-import dayjs from 'dayjs'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
 import s from './style.module.css'
@@ -12,6 +11,7 @@ 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'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type ILogs = {
   logs?: WorkflowLogsResponse
@@ -23,6 +23,7 @@ const defaultValue = 'N/A'
 
 const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
 
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
@@ -99,7 +100,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
                 setShowDrawer(true)
               }}>
               <td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
-              <td className='w-[160px]'>{dayjs.unix(log.created_at).format(t('appLog.dateTimeFormat') as string)}</td>
+              <td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
               <td>{statusTdRender(log.workflow_run.status)}</td>
               <td>
                 <div className={cn(

+ 5 - 4
web/app/components/base/agent-log-modal/result.tsx

@@ -1,10 +1,10 @@
 'use client'
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
-import dayjs from 'dayjs'
 import StatusPanel from '@/app/components/workflow/run/status'
 import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type ResultPanelProps = {
   status: string
@@ -14,7 +14,7 @@ type ResultPanelProps = {
   inputs?: any
   outputs?: any
   created_by?: string
-  created_at?: string
+  created_at: string
   agentMode?: string
   tools?: string[]
   iterations?: number
@@ -28,12 +28,13 @@ const ResultPanel: FC<ResultPanelProps> = ({
   inputs,
   outputs,
   created_by,
-  created_at = 0,
+  created_at,
   agentMode,
   tools,
   iterations,
 }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
 
   return (
     <div className='bg-white py-2'>
@@ -83,7 +84,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
             <div className='flex'>
               <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.startTime')}</div>
               <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
-                <span>{dayjs(created_at).format('YYYY-MM-DD hh:mm:ss')}</span>
+                <span>{formatTime(Date.parse(created_at) / 1000, t('appLog.dateTimeFormat') as string)}</span>
               </div>
             </div>
             <div className='flex'>

+ 4 - 2
web/app/components/base/chat/chat/hooks.ts

@@ -6,7 +6,6 @@ import {
 } from 'react'
 import { useTranslation } from 'react-i18next'
 import { produce, setAutoFreeze } from 'immer'
-import dayjs from 'dayjs'
 import type {
   ChatConfig,
   ChatItem,
@@ -20,6 +19,7 @@ import { ssePost } from '@/service/base'
 import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
 import type { Annotation } from '@/models/log'
 import { WorkflowRunningStatus } from '@/app/components/workflow/types'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type GetAbortController = (abortController: AbortController) => void
 type SendCallback = {
@@ -78,6 +78,7 @@ export const useChat = (
   stopChat?: (taskId: string) => void,
 ) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const { notify } = useToastContext()
   const connversationId = useRef('')
   const hasStopResponded = useRef(false)
@@ -336,7 +337,7 @@ export const useChat = (
                       : []),
                   ],
                   more: {
-                    time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
+                    time: formatTime(newResponseItem.created_at, 'hh:mm A'),
                     tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
                     latency: newResponseItem.provider_response_latency.toFixed(2),
                   },
@@ -498,6 +499,7 @@ export const useChat = (
     promptVariablesConfig,
     handleUpdateChatList,
     handleResponding,
+    formatTime,
   ])
 
   const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {

+ 4 - 2
web/app/components/datasets/documents/list.tsx

@@ -5,12 +5,12 @@ import React, { useEffect, useState } from 'react'
 import { useDebounceFn } from 'ahooks'
 import { ArrowDownIcon, TrashIcon } from '@heroicons/react/24/outline'
 import { ExclamationCircleIcon } from '@heroicons/react/24/solid'
-import dayjs from 'dayjs'
 import { pick } from 'lodash-es'
 import { useContext } from 'use-context-selector'
 import { useRouter } from 'next/navigation'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
+import dayjs from 'dayjs'
 import s from './style.module.css'
 import Switch from '@/app/components/base/switch'
 import Divider from '@/app/components/base/divider'
@@ -29,6 +29,7 @@ import ProgressBar from '@/app/components/base/progress-bar'
 import { DataSourceType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets'
 import type { CommonResponse } from '@/models/common'
 import { DotsHorizontal, HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
+import useTimestamp from '@/hooks/use-timestamp'
 
 export const SettingsIcon = ({ className }: SVGProps<SVGElement>) => {
   return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
@@ -305,6 +306,7 @@ type IDocumentListProps = {
  */
 const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents = [], datasetId, onUpdate }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const router = useRouter()
   const [localDocs, setLocalDocs] = useState<LocalDoc[]>(documents)
   const [enableSort, setEnableSort] = useState(false)
@@ -368,7 +370,7 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
               <td>{renderCount(doc.word_count)}</td>
               <td>{renderCount(doc.hit_count)}</td>
               <td className='text-gray-500 text-[13px]'>
-                {dayjs.unix(doc.created_at).format(t('datasetHitTesting.dateTimeFormat') as string)}
+                {formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
               </td>
               <td>
                 {

+ 3 - 2
web/app/components/datasets/hit-testing/index.tsx

@@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
 import useSWR from 'swr'
 import { omit } from 'lodash-es'
 import cn from 'classnames'
-import dayjs from 'dayjs'
 import { useBoolean } from 'ahooks'
 import { useContext } from 'use-context-selector'
 import SegmentCard from '../documents/detail/completed/SegmentCard'
@@ -24,6 +23,7 @@ import { fetchTestingRecords } from '@/service/datasets'
 import DatasetDetailContext from '@/context/dataset-detail'
 import type { RetrievalConfig } from '@/types/app'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import useTimestamp from '@/hooks/use-timestamp'
 
 const limit = 10
 
@@ -43,6 +43,7 @@ const RecordsEmpty: FC = () => {
 
 const HitTesting: FC<Props> = ({ datasetId }: Props) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
 
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
@@ -129,7 +130,7 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
                           </td>
                           <td className='max-w-xs group-hover:text-primary-600'>{record.content}</td>
                           <td className='w-36'>
-                            {dayjs.unix(record.created_at).format(t('datasetHitTesting.dateTimeFormat') as string)}
+                            {formatTime(record.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
                           </td>
                         </tr>
                       })}

+ 6 - 19
web/app/components/develop/secret-key/secret-key-modal.tsx

@@ -6,7 +6,6 @@ import {
 import { useTranslation } from 'react-i18next'
 import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
 import useSWR, { useSWRConfig } from 'swr'
-import { useContext } from 'use-context-selector'
 import copy from 'copy-to-clipboard'
 import SecretKeyGenerateModal from './secret-key-generate'
 import s from './style.module.css'
@@ -26,8 +25,7 @@ import type { CreateApiKeyResponse } from '@/models/app'
 import Tooltip from '@/app/components/base/tooltip'
 import Loading from '@/app/components/base/loading'
 import Confirm from '@/app/components/base/confirm'
-import I18n from '@/context/i18n'
-import { LanguagesSupported } from '@/i18n/language'
+import useTimestamp from '@/hooks/use-timestamp'
 import { useAppContext } from '@/context/app-context'
 
 type ISecretKeyModalProps = {
@@ -42,6 +40,7 @@ const SecretKeyModal = ({
   onClose,
 }: ISecretKeyModalProps) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
   const [showConfirmDelete, setShowConfirmDelete] = useState(false)
   const [isVisible, setVisible] = useState(false)
@@ -55,9 +54,6 @@ const SecretKeyModal = ({
 
   const [delKeyID, setDelKeyId] = useState('')
 
-  const { locale } = useContext(I18n)
-
-  // const [isCopied, setIsCopied] = useState(false)
   const [copyValue, setCopyValue] = useState('')
 
   useEffect(() => {
@@ -100,13 +96,6 @@ const SecretKeyModal = ({
     return `${token.slice(0, 3)}...${token.slice(-20)}`
   }
 
-  const formatDate = (timestamp: string) => {
-    if (locale === LanguagesSupported[0])
-      return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format((+timestamp) * 1000)
-    else
-      return new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).format((+timestamp) * 1000)
-  }
-
   return (
     <Modal isShow={isShow} onClose={onClose} title={`${t('appApi.apiKeyModal.apiSecretKey')}`} className={`${s.customModal} px-8 flex flex-col`}>
       <XMarkIcon className={`w-6 h-6 absolute cursor-pointer text-gray-500 ${s.close}`} onClick={onClose} />
@@ -117,18 +106,16 @@ const SecretKeyModal = ({
           <div className='flex flex-col flex-grow mt-4 overflow-hidden'>
             <div className='flex items-center flex-shrink-0 text-xs font-semibold text-gray-500 border-b border-solid h-9'>
               <div className='flex-shrink-0 w-64 px-3'>{t('appApi.apiKeyModal.secretKey')}</div>
-              <div className='flex-shrink-0 px-3 w-28'>{t('appApi.apiKeyModal.created')}</div>
-              <div className='flex-shrink-0 px-3 w-28'>{t('appApi.apiKeyModal.lastUsed')}</div>
+              <div className='flex-shrink-0 px-3 w-[200px]'>{t('appApi.apiKeyModal.created')}</div>
+              <div className='flex-shrink-0 px-3 w-[200px]'>{t('appApi.apiKeyModal.lastUsed')}</div>
               <div className='flex-grow px-3'></div>
             </div>
             <div className='flex-grow overflow-auto'>
               {apiKeysList.data.map(api => (
                 <div className='flex items-center text-sm font-normal text-gray-700 border-b border-solid h-9' key={api.id}>
                   <div className='flex-shrink-0 w-64 px-3 font-mono truncate'>{generateToken(api.token)}</div>
-                  <div className='flex-shrink-0 px-3 truncate w-28'>{formatDate(api.created_at)}</div>
-                  {/* <div className='flex-shrink-0 px-3 truncate w-28'>{dayjs((+api.created_at) * 1000).format('MMM D, YYYY')}</div> */}
-                  {/* <div className='flex-shrink-0 px-3 truncate w-28'>{api.last_used_at ? dayjs((+api.last_used_at) * 1000).format('MMM D, YYYY') : 'Never'}</div> */}
-                  <div className='flex-shrink-0 px-3 truncate w-28'>{api.last_used_at ? formatDate(api.last_used_at) : t('appApi.never')}</div>
+                  <div className='flex-shrink-0 px-3 truncate w-[200px]'>{formatTime(Number(api.created_at), t('appLog.dateTimeFormat') as string)}</div>
+                  <div className='flex-shrink-0 px-3 truncate w-[200px]'>{api.last_used_at ? formatTime(Number(api.created_at), t('appLog.dateTimeFormat') as string) : t('appApi.never')}</div>
                   <div className='flex flex-grow px-3'>
                     <Tooltip
                       selector={`key-${api.token}`}

+ 1 - 1
web/app/components/develop/secret-key/style.module.css

@@ -1,5 +1,5 @@
 .customModal {
-    max-width: 40rem !important;
+    max-width: 800px !important;
     max-height: calc(100vh - 80px);
 }
 

+ 3 - 2
web/app/components/workflow/header/editing-title.tsx

@@ -1,11 +1,12 @@
 import { memo } from 'react'
-import dayjs from 'dayjs'
 import { useTranslation } from 'react-i18next'
 import { useWorkflow } from '../hooks'
 import { useStore } from '@/app/components/workflow/store'
+import useTimestamp from '@/hooks/use-timestamp'
 
 const EditingTitle = () => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
   const { formatTimeFromNow } = useWorkflow()
   const draftUpdatedAt = useStore(state => state.draftUpdatedAt)
   const publishedAt = useStore(state => state.publishedAt)
@@ -15,7 +16,7 @@ const EditingTitle = () => {
       {
         !!draftUpdatedAt && (
           <>
-            {t('workflow.common.autoSaved')} {dayjs(draftUpdatedAt).format('HH:mm:ss')}
+            {t('workflow.common.autoSaved')} {formatTime(draftUpdatedAt / 1000, 'HH:mm:ss')}
           </>
         )
       }

+ 3 - 3
web/app/components/workflow/run/meta.tsx

@@ -1,8 +1,7 @@
 'use client'
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
-// import cn from 'classnames'
-import dayjs from 'dayjs'
+import useTimestamp from '@/hooks/use-timestamp'
 
 type Props = {
   status: string
@@ -24,6 +23,7 @@ const MetaData: FC<Props> = ({
   showSteps = true,
 }) => {
   const { t } = useTranslation()
+  const { formatTime } = useTimestamp()
 
   return (
     <div className='relative'>
@@ -64,7 +64,7 @@ const MetaData: FC<Props> = ({
               <div className='my-[5px] w-[72px] h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
             )}
             {status !== 'running' && (
-              <span>{dayjs(startTime * 1000).format('YYYY-MM-DD hh:mm:ss')}</span>
+              <span>{formatTime(startTime, t('appLog.dateTimeFormat') as string)}</span>
             )}
           </div>
         </div>

+ 5 - 3
web/hooks/use-metadata.ts

@@ -1,8 +1,8 @@
 'use client'
 import { useTranslation } from 'react-i18next'
-import dayjs from 'dayjs'
 import { formatFileSize, formatNumber, formatTime } from '@/utils/format'
 import type { DocType } from '@/models/datasets'
+import useTimestamp from '@/hooks/use-timestamp'
 
 export type inputType = 'input' | 'select' | 'textarea'
 export type metadataType = DocType | 'originInfo' | 'technicalParameters'
@@ -31,6 +31,8 @@ const fieldPrefix = 'datasetDocuments.metadata.field'
 
 export const useMetadataMap = (): MetadataMap => {
   const { t } = useTranslation()
+  const { formatTime: formatTimestamp } = useTimestamp()
+
   return {
     book: {
       text: t('datasetDocuments.metadata.type.book'),
@@ -230,11 +232,11 @@ export const useMetadataMap = (): MetadataMap => {
         },
         'created_at': {
           label: t(`${fieldPrefix}.originInfo.uploadDate`),
-          render: value => dayjs.unix(value).format(t('datasetDocuments.metadata.dateTimeFormat') as string),
+          render: value => formatTimestamp(value, t('datasetDocuments.metadata.dateTimeFormat') as string),
         },
         'completed_at': {
           label: t(`${fieldPrefix}.originInfo.lastUpdateDate`),
-          render: value => dayjs.unix(value).format(t('datasetDocuments.metadata.dateTimeFormat') as string),
+          render: value => formatTimestamp(value, t('datasetDocuments.metadata.dateTimeFormat') as string),
         },
         'data_source_type': {
           label: t(`${fieldPrefix}.originInfo.source`),

+ 21 - 0
web/hooks/use-timestamp.ts

@@ -0,0 +1,21 @@
+'use client'
+import { useCallback } from 'react'
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+import timezone from 'dayjs/plugin/timezone'
+import { useAppContext } from '@/context/app-context'
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+
+const useTimestamp = () => {
+  const { userProfile: { timezone } } = useAppContext()
+
+  const formatTime = useCallback((value: number, format: string) => {
+    return dayjs.unix(value).tz(timezone).format(format)
+  }, [timezone])
+
+  return { formatTime }
+}
+
+export default useTimestamp