Co-authored-by: wangpj <wangpj@hundsunc.om> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: -LAN- <laipz8200@outlook.com>tags/0.7.2
| @@ -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, | |||
| @@ -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.") | |||
| @@ -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.") | |||
| @@ -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, | |||
| @@ -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, | |||
| @@ -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() | |||
| @@ -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 | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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 | |||
| @@ -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> | |||
| @@ -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 | |||
| @@ -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', | |||
| @@ -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.', | |||
| @@ -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', | |||
| @@ -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 の操作を記録しました。', | |||
| @@ -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: '日志记录了应用的执行情况', | |||
| @@ -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 | |||