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>
This commit is contained in:
KinWang 2024-08-20 17:55:44 +08:00 committed by GitHub
parent eae53e11e6
commit e35e251863
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 227 additions and 51 deletions

View File

@ -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('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('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('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() args = parser.parse_args()
subquery = ( subquery = (
@ -225,7 +227,17 @@ class ChatConversationApi(Resource):
if app_model.mode == AppMode.ADVANCED_CHAT.value: if app_model.mode == AppMode.ADVANCED_CHAT.value:
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER.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( conversations = db.paginate(
query, query,

View File

@ -25,6 +25,8 @@ class ConversationApi(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('last_id', type=uuid_value, location='args') 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('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() args = parser.parse_args()
try: try:
@ -33,7 +35,8 @@ class ConversationApi(Resource):
user=end_user, user=end_user,
last_id=args['last_id'], last_id=args['last_id'],
limit=args['limit'], limit=args['limit'],
invoke_from=InvokeFrom.SERVICE_API invoke_from=InvokeFrom.SERVICE_API,
sort_by=args['sort_by']
) )
except services.errors.conversation.LastConversationNotExistsError: except services.errors.conversation.LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.") raise NotFound("Last Conversation Not Exists.")

View File

@ -26,6 +26,8 @@ class ConversationListApi(WebApiResource):
parser.add_argument('last_id', type=uuid_value, location='args') 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('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('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() args = parser.parse_args()
pinned = None pinned = None
@ -40,6 +42,7 @@ class ConversationListApi(WebApiResource):
limit=args['limit'], limit=args['limit'],
invoke_from=InvokeFrom.WEB_APP, invoke_from=InvokeFrom.WEB_APP,
pinned=pinned, pinned=pinned,
sort_by=args['sort_by']
) )
except LastConversationNotExistsError: except LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.") raise NotFound("Last Conversation Not Exists.")

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import type { QueryParam } from './index' import type { QueryParam } from './index'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import Sort from '@/app/components/base/sort'
import { fetchAnnotationsCount } from '@/service/log' import { fetchAnnotationsCount } from '@/service/log'
dayjs.extend(quarterOfYear) dayjs.extend(quarterOfYear)
@ -28,18 +29,19 @@ export const TIME_PERIOD_LIST = [
] ]
type IFilterProps = { type IFilterProps = {
isChatMode?: boolean
appId: string appId: string
queryParams: QueryParam queryParams: QueryParam
setQueryParams: (v: QueryParam) => void 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 { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount)
const { t } = useTranslation() const { t } = useTranslation()
if (!data) if (!data)
return null return null
return ( 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 <SimpleSelect
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))} items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
className='mt-0 !w-40' className='mt-0 !w-40'
@ -68,7 +70,7 @@ const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilte
<input <input
type="text" type="text"
name="query" 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')!} placeholder={t('common.operation.search')!}
value={queryParams.keyword} value={queryParams.keyword}
onChange={(e) => { onChange={(e) => {
@ -76,6 +78,22 @@ const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilte
}} }}
/> />
</div> </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> </div>
) )
} }

View File

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

View File

@ -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"> <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
<tr> <tr>
<td className='w-[1.375rem] whitespace-nowrap'></td> <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'>{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'>{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.userRate')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</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> </tr>
</thead> </thead>
<tbody className="text-gray-500"> <tbody className="text-gray-500">
@ -692,11 +693,10 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
setCurrentConversation(log) 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='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 }}> <td style={{ maxWidth: isChatMode ? 300 : 200 }}>
{renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)} {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
</td> </td>
<td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
<td style={{ maxWidth: isChatMode ? 100 : 200 }}> <td style={{ maxWidth: isChatMode ? 100 : 200 }}>
{renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)} {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
</td> </td>
@ -718,6 +718,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
</> </>
} }
</td> </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> </tr>
})} })}
</tbody> </tbody>

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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