Przeglądaj źródła

feat: Sort conversations by updated_at desc (#7348)

Co-authored-by: wangpj <wangpj@hundsunc.om>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
KinWang 8 miesięcy temu
rodzic
commit
e35e251863

+ 13 - 1
api/controllers/console/app/conversation.py

@@ -154,6 +154,8 @@ class ChatConversationApi(Resource):
         parser.add_argument('message_count_gte', type=int_range(1, 99999), required=False, location='args')
         parser.add_argument('page', type=int_range(1, 99999), required=False, default=1, location='args')
         parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
+        parser.add_argument('sort_by', type=str, choices=['created_at', '-created_at', 'updated_at', '-updated_at'],
+                            required=False, default='-updated_at', location='args')
         args = parser.parse_args()
 
         subquery = (
@@ -225,7 +227,17 @@ class ChatConversationApi(Resource):
         if app_model.mode == AppMode.ADVANCED_CHAT.value:
             query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER.value)
 
-        query = query.order_by(Conversation.created_at.desc())
+        match args['sort_by']:
+            case 'created_at':
+                query = query.order_by(Conversation.created_at.asc())
+            case '-created_at':
+                query = query.order_by(Conversation.created_at.desc())
+            case 'updated_at':
+                query = query.order_by(Conversation.updated_at.asc())
+            case '-updated_at':
+                query = query.order_by(Conversation.updated_at.desc())
+            case _:
+                query = query.order_by(Conversation.created_at.desc())
 
         conversations = db.paginate(
             query,

+ 4 - 1
api/controllers/service_api/app/conversation.py

@@ -25,6 +25,8 @@ class ConversationApi(Resource):
         parser = reqparse.RequestParser()
         parser.add_argument('last_id', type=uuid_value, location='args')
         parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
+        parser.add_argument('sort_by', type=str, choices=['created_at', '-created_at', 'updated_at', '-updated_at'],
+                            required=False, default='-updated_at', location='args')
         args = parser.parse_args()
 
         try:
@@ -33,7 +35,8 @@ class ConversationApi(Resource):
                 user=end_user,
                 last_id=args['last_id'],
                 limit=args['limit'],
-                invoke_from=InvokeFrom.SERVICE_API
+                invoke_from=InvokeFrom.SERVICE_API,
+                sort_by=args['sort_by']
             )
         except services.errors.conversation.LastConversationNotExistsError:
             raise NotFound("Last Conversation Not Exists.")

+ 3 - 0
api/controllers/web/conversation.py

@@ -26,6 +26,8 @@ class ConversationListApi(WebApiResource):
         parser.add_argument('last_id', type=uuid_value, location='args')
         parser.add_argument('limit', type=int_range(1, 100), required=False, default=20, location='args')
         parser.add_argument('pinned', type=str, choices=['true', 'false', None], location='args')
+        parser.add_argument('sort_by', type=str, choices=['created_at', '-created_at', 'updated_at', '-updated_at'],
+                            required=False, default='-updated_at', location='args')
         args = parser.parse_args()
 
         pinned = None
@@ -40,6 +42,7 @@ class ConversationListApi(WebApiResource):
                 limit=args['limit'],
                 invoke_from=InvokeFrom.WEB_APP,
                 pinned=pinned,
+                sort_by=args['sort_by']
             )
         except LastConversationNotExistsError:
             raise NotFound("Last Conversation Not Exists.")

+ 15 - 11
api/core/app/apps/message_based_app_generator.py

@@ -1,6 +1,7 @@
 import json
 import logging
 from collections.abc import Generator
+from datetime import datetime, timezone
 from typing import Optional, Union
 
 from sqlalchemy import and_
@@ -36,17 +37,17 @@ logger = logging.getLogger(__name__)
 class MessageBasedAppGenerator(BaseAppGenerator):
 
     def _handle_response(
-        self, application_generate_entity: Union[
-            ChatAppGenerateEntity,
-            CompletionAppGenerateEntity,
-            AgentChatAppGenerateEntity,
-            AdvancedChatAppGenerateEntity
-        ],
-        queue_manager: AppQueueManager,
-        conversation: Conversation,
-        message: Message,
-        user: Union[Account, EndUser],
-        stream: bool = False,
+            self, application_generate_entity: Union[
+                ChatAppGenerateEntity,
+                CompletionAppGenerateEntity,
+                AgentChatAppGenerateEntity,
+                AdvancedChatAppGenerateEntity
+            ],
+            queue_manager: AppQueueManager,
+            conversation: Conversation,
+            message: Message,
+            user: Union[Account, EndUser],
+            stream: bool = False,
     ) -> Union[
         ChatbotAppBlockingResponse,
         CompletionAppBlockingResponse,
@@ -193,6 +194,9 @@ class MessageBasedAppGenerator(BaseAppGenerator):
             db.session.add(conversation)
             db.session.commit()
             db.session.refresh(conversation)
+        else:
+            conversation.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
+            db.session.commit()
 
         message = Message(
             app_id=app_config.app_id,

+ 1 - 0
api/fields/conversation_fields.py

@@ -150,6 +150,7 @@ conversation_with_summary_fields = {
     "summary": fields.String(attribute="summary_or_query"),
     "read_at": TimestampField,
     "created_at": TimestampField,
+    "updated_at": TimestampField,
     "annotated": fields.Boolean,
     "model_config": fields.Nested(simple_model_config_fields),
     "message_count": fields.Integer,

+ 38 - 20
api/services/conversation_service.py

@@ -1,6 +1,7 @@
+from datetime import datetime, timezone
 from typing import Optional, Union
 
-from sqlalchemy import or_
+from sqlalchemy import asc, desc, or_
 
 from core.app.entities.app_invoke_entities import InvokeFrom
 from core.llm_generator.llm_generator import LLMGenerator
@@ -18,7 +19,8 @@ class ConversationService:
                               last_id: Optional[str], limit: int,
                               invoke_from: InvokeFrom,
                               include_ids: Optional[list] = None,
-                              exclude_ids: Optional[list] = None) -> InfiniteScrollPagination:
+                              exclude_ids: Optional[list] = None,
+                              sort_by: str = '-updated_at') -> InfiniteScrollPagination:
         if not user:
             return InfiniteScrollPagination(data=[], limit=limit, has_more=False)
 
@@ -37,28 +39,28 @@ class ConversationService:
         if exclude_ids is not None:
             base_query = base_query.filter(~Conversation.id.in_(exclude_ids))
 
-        if last_id:
-            last_conversation = base_query.filter(
-                Conversation.id == last_id,
-            ).first()
+        # define sort fields and directions
+        sort_field, sort_direction = cls._get_sort_params(sort_by)
 
+        if last_id:
+            last_conversation = base_query.filter(Conversation.id == last_id).first()
             if not last_conversation:
                 raise LastConversationNotExistsError()
 
-            conversations = base_query.filter(
-                Conversation.created_at < last_conversation.created_at,
-                Conversation.id != last_conversation.id
-            ).order_by(Conversation.created_at.desc()).limit(limit).all()
-        else:
-            conversations = base_query.order_by(Conversation.created_at.desc()).limit(limit).all()
+            # build filters based on sorting
+            filter_condition = cls._build_filter_condition(sort_field, sort_direction, last_conversation)
+            base_query = base_query.filter(filter_condition)
+
+        base_query = base_query.order_by(sort_direction(getattr(Conversation, sort_field)))
+
+        conversations = base_query.limit(limit).all()
 
         has_more = False
         if len(conversations) == limit:
-            current_page_first_conversation = conversations[-1]
-            rest_count = base_query.filter(
-                Conversation.created_at < current_page_first_conversation.created_at,
-                Conversation.id != current_page_first_conversation.id
-            ).count()
+            current_page_last_conversation = conversations[-1]
+            rest_filter_condition = cls._build_filter_condition(sort_field, sort_direction,
+                                                                current_page_last_conversation, is_next_page=True)
+            rest_count = base_query.filter(rest_filter_condition).count()
 
             if rest_count > 0:
                 has_more = True
@@ -69,6 +71,21 @@ class ConversationService:
             has_more=has_more
         )
 
+    @classmethod
+    def _get_sort_params(cls, sort_by: str) -> tuple[str, callable]:
+        if sort_by.startswith('-'):
+            return sort_by[1:], desc
+        return sort_by, asc
+
+    @classmethod
+    def _build_filter_condition(cls, sort_field: str, sort_direction: callable, reference_conversation: Conversation,
+                                is_next_page: bool = False):
+        field_value = getattr(reference_conversation, sort_field)
+        if (sort_direction == desc and not is_next_page) or (sort_direction == asc and is_next_page):
+            return getattr(Conversation, sort_field) < field_value
+        else:
+            return getattr(Conversation, sort_field) > field_value
+
     @classmethod
     def rename(cls, app_model: App, conversation_id: str,
                user: Optional[Union[Account, EndUser]], name: str, auto_generate: bool):
@@ -78,6 +95,7 @@ class ConversationService:
             return cls.auto_generate_name(app_model, conversation)
         else:
             conversation.name = name
+            conversation.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
             db.session.commit()
 
         return conversation
@@ -87,9 +105,9 @@ class ConversationService:
         # get conversation first message
         message = db.session.query(Message) \
             .filter(
-                Message.app_id == app_model.id,
-                Message.conversation_id == conversation.id
-            ).order_by(Message.created_at.asc()).first()
+            Message.app_id == app_model.id,
+            Message.conversation_id == conversation.id
+        ).order_by(Message.created_at.asc()).first()
 
         if not message:
             raise MessageNotExistsError()

+ 3 - 1
api/services/web_conversation_service.py

@@ -13,7 +13,8 @@ class WebConversationService:
     @classmethod
     def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account, EndUser]],
                               last_id: Optional[str], limit: int, invoke_from: InvokeFrom,
-                              pinned: Optional[bool] = None) -> InfiniteScrollPagination:
+                              pinned: Optional[bool] = None,
+                              sort_by='-updated_at') -> InfiniteScrollPagination:
         include_ids = None
         exclude_ids = None
         if pinned is not None:
@@ -36,6 +37,7 @@ class WebConversationService:
             invoke_from=invoke_from,
             include_ids=include_ids,
             exclude_ids=exclude_ids,
+            sort_by=sort_by
         )
 
     @classmethod

+ 21 - 3
web/app/components/app/log/filter.tsx

@@ -10,6 +10,7 @@ import dayjs from 'dayjs'
 import quarterOfYear from 'dayjs/plugin/quarterOfYear'
 import type { QueryParam } from './index'
 import { SimpleSelect } from '@/app/components/base/select'
+import Sort from '@/app/components/base/sort'
 import { fetchAnnotationsCount } from '@/service/log'
 dayjs.extend(quarterOfYear)
 
@@ -28,18 +29,19 @@ export const TIME_PERIOD_LIST = [
 ]
 
 type IFilterProps = {
+  isChatMode?: boolean
   appId: string
   queryParams: QueryParam
   setQueryParams: (v: QueryParam) => void
 }
 
-const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilterProps) => {
+const Filter: FC<IFilterProps> = ({ isChatMode, appId, queryParams, setQueryParams }: IFilterProps) => {
   const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount)
   const { t } = useTranslation()
   if (!data)
     return null
   return (
-    <div className='flex flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
+    <div className='flex flex-row flex-wrap gap-2 items-center mb-4 text-gray-900 text-base'>
       <SimpleSelect
         items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
         className='mt-0 !w-40'
@@ -68,7 +70,7 @@ const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilte
         <input
           type="text"
           name="query"
-          className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
+          className="block w-[180px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
           placeholder={t('common.operation.search')!}
           value={queryParams.keyword}
           onChange={(e) => {
@@ -76,6 +78,22 @@ const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilte
           }}
         />
       </div>
+      {isChatMode && (
+        <>
+          <div className='w-px h-3.5 bg-divider-regular'></div>
+          <Sort
+            order={queryParams.sort_by?.startsWith('-') ? '-' : ''}
+            value={queryParams.sort_by?.replace('-', '') || 'created_at'}
+            items={[
+              { value: 'created_at', name: t('appLog.table.header.time') },
+              { value: 'updated_at', name: t('appLog.table.header.updatedTime') },
+            ]}
+            onSelect={(value) => {
+              setQueryParams({ ...queryParams, sort_by: value as string })
+            }}
+          />
+        </>
+      )}
     </div>
   )
 }

+ 11 - 5
web/app/components/app/log/index.tsx

@@ -24,6 +24,7 @@ export type QueryParam = {
   period?: number | string
   annotation_status?: string
   keyword?: string
+  sort_by?: string
 }
 
 const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
@@ -52,9 +53,16 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
 
 const Logs: FC<ILogsProps> = ({ appDetail }) => {
   const { t } = useTranslation()
-  const [queryParams, setQueryParams] = useState<QueryParam>({ period: 7, annotation_status: 'all' })
+  const [queryParams, setQueryParams] = useState<QueryParam>({
+    period: 7,
+    annotation_status: 'all',
+    sort_by: '-created_at',
+  })
   const [currPage, setCurrPage] = React.useState<number>(0)
 
+  // Get the app type first
+  const isChatMode = appDetail.mode !== 'completion'
+
   const query = {
     page: currPage + 1,
     limit: APP_PAGE_LIMIT,
@@ -64,6 +72,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
         end: dayjs().endOf('day').format('YYYY-MM-DD HH:mm'),
       }
       : {}),
+    ...(isChatMode ? { sort_by: queryParams.sort_by } : {}),
     ...omit(queryParams, ['period']),
   }
 
@@ -73,9 +82,6 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
     return appType
   }
 
-  // Get the app type first
-  const isChatMode = appDetail.mode !== 'completion'
-
   // When the details are obtained, proceed to the next request
   const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode
     ? {
@@ -97,7 +103,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
     <div className='flex flex-col h-full'>
       <p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
       <div className='flex flex-col py-4 flex-1'>
-        <Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
+        <Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
         {total === undefined
           ? <Loading type='app' />
           : total > 0

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

@@ -671,12 +671,13 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
         <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
           <tr>
             <td className='w-[1.375rem] whitespace-nowrap'></td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
             <td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
+            <td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
             <td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
             <td className='whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
             <td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
+            <td className='whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
+            <td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
           </tr>
         </thead>
         <tbody className="text-gray-500">
@@ -692,11 +693,10 @@ 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]'>{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)}
               </td>
+              <td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
               <td style={{ maxWidth: isChatMode ? 100 : 200 }}>
                 {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
               </td>
@@ -718,6 +718,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
                   </>
                 }
               </td>
+              <td className='w-[160px]'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
+              <td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
             </tr>
           })}
         </tbody>

+ 92 - 0
web/app/components/base/sort/index.tsx

@@ -0,0 +1,92 @@
+import type { FC } from 'react'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { RiArrowDownSLine, RiCheckLine, RiSortAsc, RiSortDesc } from '@remixicon/react'
+import cn from '@/utils/classnames'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+export type Item = {
+  value: number | string
+  name: string
+} & Record<string, any>
+
+type Props = {
+  order?: string
+  value: number | string
+  items: Item[]
+  onSelect: (item: any) => void
+}
+const Sort: FC<Props> = ({
+  order,
+  value,
+  items,
+  onSelect,
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  const triggerContent = useMemo(() => {
+    return items.find(item => item.value === value)?.name || ''
+  }, [items, value])
+
+  return (
+    <div className='inline-flex items-center'>
+      <PortalToFollowElem
+        open={open}
+        onOpenChange={setOpen}
+        placement='bottom-start'
+        offset={4}
+      >
+        <div className='relative'>
+          <PortalToFollowElemTrigger
+            onClick={() => setOpen(v => !v)}
+            className='block'
+          >
+            <div className={cn(
+              'flex items-center px-2 py-1.5 rounded-l-lg bg-components-input-bg-normal cursor-pointer hover:bg-state-base-hover-alt',
+              open && '!bg-state-base-hover-alt hover:bg-state-base-hover-alt',
+            )}>
+              <div className='p-1 flex items-center gap-0.5'>
+                <div className='text-text-tertiary system-sm-regular'>{t('appLog.filter.sortBy')}</div>
+                <div className={cn('system-sm-regular text-text-tertiary', !!value && 'text-text-secondary')}>
+                  {triggerContent}
+                </div>
+              </div>
+              <RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
+            </div>
+          </PortalToFollowElemTrigger>
+          <PortalToFollowElemContent className='z-[1002]'>
+            <div className='relative w-[240px] bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
+              <div className='p-1 max-h-72 overflow-auto'>
+                {items.map(item => (
+                  <div
+                    key={item.value}
+                    className='flex items-center gap-2 pl-3 py-[6px] px-2 rounded-lg cursor-pointer hover:bg-state-base-hover'
+                    onClick={() => {
+                      onSelect(`${order}${item.value}`)
+                      setOpen(false)
+                    }}
+                  >
+                    <div title={item.name} className='grow text-text-secondary system-sm-medium truncate'>{item.name}</div>
+                    {value === item.value && <RiCheckLine className='shrink-0 w-4 h-4 text-util-colors-blue-light-blue-light-600' />}
+                  </div>
+                ))}
+              </div>
+            </div>
+          </PortalToFollowElemContent>
+        </div>
+      </PortalToFollowElem>
+      <div className='ml-px p-2.5 rounded-r-lg bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover cursor-pointer' onClick={() => onSelect(`${order ? '' : '-'}${value}`)}>
+        {!order && <RiSortAsc className='w-4 h-4 text-components-button-tertiary-text' />}
+        {order && <RiSortDesc className='w-4 h-4 text-components-button-tertiary-text' />}
+      </div>
+    </div>
+
+  )
+}
+
+export default Sort

+ 2 - 1
web/i18n/de-DE/app-log.ts

@@ -4,7 +4,8 @@ const translation = {
   dateTimeFormat: 'MM/DD/YYYY hh:mm A',
   table: {
     header: {
-      time: 'Zeit',
+      updatedTime: 'Aktualisierungszeit',
+      time: 'Erstellungszeit',
       endUser: 'Endbenutzer',
       input: 'Eingabe',
       output: 'Ausgabe',

+ 5 - 1
web/i18n/en-US/app-log.ts

@@ -4,7 +4,8 @@ const translation = {
   dateTimeFormat: 'MM/DD/YYYY hh:mm A',
   table: {
     header: {
-      time: 'Time',
+      updatedTime: 'Updated time',
+      time: 'Created time',
       endUser: 'End User',
       input: 'Input',
       output: 'Output',
@@ -69,6 +70,9 @@ const translation = {
       annotated: 'Annotated Improvements ({{count}} items)',
       not_annotated: 'Not Annotated',
     },
+    sortBy: 'Sort by:',
+    descending: 'descending',
+    ascending: 'ascending',
   },
   workflowTitle: 'Workflow Logs',
   workflowSubtitle: 'The log recorded the operation of Automate.',

+ 2 - 1
web/i18n/es-ES/app-log.ts

@@ -4,7 +4,8 @@ const translation = {
   dateTimeFormat: 'MM/DD/YYYY hh:mm A',
   table: {
     header: {
-      time: 'Tiempo',
+      updatedTime: 'Hora actualizada',
+      time: 'Hora creada',
       endUser: 'Usuario Final',
       input: 'Entrada',
       output: 'Salida',

+ 5 - 1
web/i18n/ja-JP/app-log.ts

@@ -4,7 +4,8 @@ const translation = {
   dateTimeFormat: 'MM/DD/YYYY hh:mm A',
   table: {
     header: {
-      time: '時間',
+      updatedTime: '更新時間',
+      time: '作成時間',
       endUser: 'エンドユーザー',
       input: '入力',
       output: '出力',
@@ -69,6 +70,9 @@ const translation = {
       annotated: '注釈付きの改善 ({{count}} アイテム)',
       not_annotated: '注釈なし',
     },
+    sortBy: '並べ替え',
+    descending: '降順',
+    ascending: '昇順',
   },
   workflowTitle: 'ワークフローログ',
   workflowSubtitle: 'このログは Automate の操作を記録しました。',

+ 5 - 1
web/i18n/zh-Hans/app-log.ts

@@ -4,7 +4,8 @@ const translation = {
   dateTimeFormat: 'YYYY-MM-DD HH:mm',
   table: {
     header: {
-      time: '时间',
+      updatedTime: '更新时间',
+      time: '创建时间',
       endUser: '用户',
       input: '输入',
       output: '输出',
@@ -69,6 +70,9 @@ const translation = {
       annotated: '已标注改进({{count}} 项)',
       not_annotated: '未标注',
     },
+    sortBy: '排序:',
+    descending: '降序',
+    ascending: '升序',
   },
   workflowTitle: '日志',
   workflowSubtitle: '日志记录了应用的执行情况',

+ 1 - 0
web/models/log.ts

@@ -117,6 +117,7 @@ export type CompletionConversationGeneralDetail = {
   from_account_id: string
   read_at: Date
   created_at: number
+  updated_at: number
   annotation: Annotation
   user_feedback_stats: {
     like: number