From e35e25186356e04b16d861079cf9a335a3aa2735 Mon Sep 17 00:00:00 2001 From: KinWang <33823007+KinWang130@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:55:44 +0800 Subject: [PATCH] feat: Sort conversations by updated_at desc (#7348) Co-authored-by: wangpj Co-authored-by: JzoNg Co-authored-by: -LAN- --- api/controllers/console/app/conversation.py | 14 ++- .../service_api/app/conversation.py | 5 +- api/controllers/web/conversation.py | 3 + .../app/apps/message_based_app_generator.py | 26 +++--- api/fields/conversation_fields.py | 1 + api/services/conversation_service.py | 58 ++++++++---- api/services/web_conversation_service.py | 4 +- web/app/components/app/log/filter.tsx | 24 ++++- web/app/components/app/log/index.tsx | 16 +++- web/app/components/app/log/list.tsx | 10 +- web/app/components/base/sort/index.tsx | 92 +++++++++++++++++++ web/i18n/de-DE/app-log.ts | 3 +- web/i18n/en-US/app-log.ts | 6 +- web/i18n/es-ES/app-log.ts | 3 +- web/i18n/ja-JP/app-log.ts | 6 +- web/i18n/zh-Hans/app-log.ts | 6 +- web/models/log.ts | 1 + 17 files changed, 227 insertions(+), 51 deletions(-) create mode 100644 web/app/components/base/sort/index.tsx diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index eb61c83d46..995264541f 100644 --- a/api/controllers/console/app/conversation.py +++ b/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, diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 44bda8e771..4598a14611 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/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.") diff --git a/api/controllers/web/conversation.py b/api/controllers/web/conversation.py index b83ea3a525..334ee382a2 100644 --- a/api/controllers/web/conversation.py +++ b/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.") diff --git a/api/core/app/apps/message_based_app_generator.py b/api/core/app/apps/message_based_app_generator.py index 49ef7b7b40..fceed95b91 100644 --- a/api/core/app/apps/message_based_app_generator.py +++ b/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, diff --git a/api/fields/conversation_fields.py b/api/fields/conversation_fields.py index 1b15fe3880..3a64801e18 100644 --- a/api/fields/conversation_fields.py +++ b/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, diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index 82ee10ee78..053d704e17 100644 --- a/api/services/conversation_service.py +++ b/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() diff --git a/api/services/web_conversation_service.py b/api/services/web_conversation_service.py index cba048ccdb..269a048134 100644 --- a/api/services/web_conversation_service.py +++ b/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 diff --git a/web/app/components/app/log/filter.tsx b/web/app/components/app/log/filter.tsx index 80a58bb5a2..0552b44d16 100644 --- a/web/app/components/app/log/filter.tsx +++ b/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 = ({ appId, queryParams, setQueryParams }: IFilterProps) => { +const Filter: FC = ({ isChatMode, appId, queryParams, setQueryParams }: IFilterProps) => { const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount) const { t } = useTranslation() if (!data) return null return ( -
+
({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))} className='mt-0 !w-40' @@ -68,7 +70,7 @@ const Filter: FC = ({ appId, queryParams, setQueryParams }: IFilte { @@ -76,6 +78,22 @@ const Filter: FC = ({ appId, queryParams, setQueryParams }: IFilte }} />
+ {isChatMode && ( + <> +
+ { + setQueryParams({ ...queryParams, sort_by: value as string }) + }} + /> + + )}
) } diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index e9ad2f43c6..dd6ebd08f0 100644 --- a/web/app/components/app/log/index.tsx +++ b/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) => { @@ -52,9 +53,16 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => { const Logs: FC = ({ appDetail }) => { const { t } = useTranslation() - const [queryParams, setQueryParams] = useState({ period: 7, annotation_status: 'all' }) + const [queryParams, setQueryParams] = useState({ + period: 7, + annotation_status: 'all', + sort_by: '-created_at', + }) const [currPage, setCurrPage] = React.useState(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 = ({ 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 = ({ 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 = ({ appDetail }) => {

{t('appLog.description')}

- + {total === undefined ? : total > 0 diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 646ae80116..40d171761b 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -671,12 +671,13 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) - {t('appLog.table.header.time')} - {t('appLog.table.header.endUser')} {isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')} + {t('appLog.table.header.endUser')} {isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')} {t('appLog.table.header.userRate')} {t('appLog.table.header.adminRate')} + {t('appLog.table.header.updatedTime')} + {t('appLog.table.header.time')} @@ -692,11 +693,10 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) setCurrentConversation(log) }}> {!log.read_at && } - {formatTime(log.created_at, t('appLog.dateTimeFormat') as string)} - {renderTdValue(endUser || defaultValue, !endUser)} {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)} + {renderTdValue(endUser || defaultValue, !endUser)} {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)} @@ -718,6 +718,8 @@ const ConversationList: FC = ({ logs, appDetail, onRefresh }) } + {formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)} + {formatTime(log.created_at, t('appLog.dateTimeFormat') as string)} })} diff --git a/web/app/components/base/sort/index.tsx b/web/app/components/base/sort/index.tsx new file mode 100644 index 0000000000..b2524d8516 --- /dev/null +++ b/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 + +type Props = { + order?: string + value: number | string + items: Item[] + onSelect: (item: any) => void +} +const Sort: FC = ({ + 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 ( +
+ +
+ setOpen(v => !v)} + className='block' + > +
+
+
{t('appLog.filter.sortBy')}
+
+ {triggerContent} +
+
+ +
+
+ +
+
+ {items.map(item => ( +
{ + onSelect(`${order}${item.value}`) + setOpen(false) + }} + > +
{item.name}
+ {value === item.value && } +
+ ))} +
+
+
+
+
+
onSelect(`${order ? '' : '-'}${value}`)}> + {!order && } + {order && } +
+
+ + ) +} + +export default Sort diff --git a/web/i18n/de-DE/app-log.ts b/web/i18n/de-DE/app-log.ts index f0985a2ed7..0660a87c91 100644 --- a/web/i18n/de-DE/app-log.ts +++ b/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', diff --git a/web/i18n/en-US/app-log.ts b/web/i18n/en-US/app-log.ts index b45c1640d1..17a6f4755a 100644 --- a/web/i18n/en-US/app-log.ts +++ b/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.', diff --git a/web/i18n/es-ES/app-log.ts b/web/i18n/es-ES/app-log.ts index 2a6c9f57da..cd957239b2 100644 --- a/web/i18n/es-ES/app-log.ts +++ b/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', diff --git a/web/i18n/ja-JP/app-log.ts b/web/i18n/ja-JP/app-log.ts index 9d5ef54be8..806544f2de 100644 --- a/web/i18n/ja-JP/app-log.ts +++ b/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 の操作を記録しました。', diff --git a/web/i18n/zh-Hans/app-log.ts b/web/i18n/zh-Hans/app-log.ts index d8993c8f75..47ed566085 100644 --- a/web/i18n/zh-Hans/app-log.ts +++ b/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: '日志记录了应用的执行情况', diff --git a/web/models/log.ts b/web/models/log.ts index fbd4674c9b..6f8ebb1a78 100644 --- a/web/models/log.ts +++ b/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