diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 06b60a25..10b141c0 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -1478,90 +1478,17 @@ class GetFeedbackAnalytics(Resource): ) except Exception as err: return make_response(jsonify({"success": False, "error": str(err)}), 400) + end_date = datetime.datetime.now(datetime.timezone.utc) if filter_option == "last_hour": start_date = end_date - datetime.timedelta(hours=1) group_format = "%Y-%m-%d %H:%M:00" - group_stage_1 = { - "$group": { - "_id": { - "minute": { - "$dateToString": { - "format": group_format, - "date": "$timestamp", - } - }, - "feedback": "$feedback", - }, - "count": {"$sum": 1}, - } - } - group_stage_2 = { - "$group": { - "_id": "$_id.minute", - "likes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "LIKE"]}, - "$count", - 0, - ] - } - }, - "dislikes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "DISLIKE"]}, - "$count", - 0, - ] - } - }, - } - } - + date_field = {"$dateToString": {"format": group_format, "date": "$date"}} elif filter_option == "last_24_hour": start_date = end_date - datetime.timedelta(hours=24) group_format = "%Y-%m-%d %H:00" - group_stage_1 = { - "$group": { - "_id": { - "hour": { - "$dateToString": { - "format": group_format, - "date": "$timestamp", - } - }, - "feedback": "$feedback", - }, - "count": {"$sum": 1}, - } - } - group_stage_2 = { - "$group": { - "_id": "$_id.hour", - "likes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "LIKE"]}, - "$count", - 0, - ] - } - }, - "dislikes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "DISLIKE"]}, - "$count", - 0, - ] - } - }, - } - } - + date_field = {"$dateToString": {"format": group_format, "date": "$date"}} else: if filter_option in ["last_7_days", "last_15_days", "last_30_days"]: filter_days = ( @@ -1579,61 +1506,59 @@ class GetFeedbackAnalytics(Resource): hour=23, minute=59, second=59, microsecond=999999 ) group_format = "%Y-%m-%d" - group_stage_1 = { - "$group": { - "_id": { - "day": { - "$dateToString": { - "format": group_format, - "date": "$timestamp", - } - }, - "feedback": "$feedback", - }, - "count": {"$sum": 1}, - } - } - group_stage_2 = { - "$group": { - "_id": "$_id.day", - "likes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "LIKE"]}, - "$count", - 0, - ] - } - }, - "dislikes": { - "$sum": { - "$cond": [ - {"$eq": ["$_id.feedback", "DISLIKE"]}, - "$count", - 0, - ] - } - }, - } - } + date_field = {"$dateToString": {"format": group_format, "date": "$date"}} try: match_stage = { "$match": { - "timestamp": {"$gte": start_date, "$lte": end_date}, + "date": {"$gte": start_date, "$lte": end_date}, + "queries": {"$exists": True, "$ne": []}, } } if api_key: match_stage["$match"]["api_key"] = api_key - feedback_data = feedback_collection.aggregate( - [ - match_stage, - group_stage_1, - group_stage_2, - {"$sort": {"_id": 1}}, - ] - ) + # Unwind the queries array to process each query separately + pipeline = [ + match_stage, + {"$unwind": "$queries"}, + {"$match": {"queries.feedback": {"$exists": True}}}, + { + "$group": { + "_id": { + "time": date_field, + "feedback": "$queries.feedback" + }, + "count": {"$sum": 1} + } + }, + { + "$group": { + "_id": "$_id.time", + "positive": { + "$sum": { + "$cond": [ + {"$eq": ["$_id.feedback", "LIKE"]}, + "$count", + 0 + ] + } + }, + "negative": { + "$sum": { + "$cond": [ + {"$eq": ["$_id.feedback", "DISLIKE"]}, + "$count", + 0 + ] + } + } + } + }, + {"$sort": {"_id": 1}} + ] + + feedback_data = conversations_collection.aggregate(pipeline) if filter_option == "last_hour": intervals = generate_minute_range(start_date, end_date) @@ -1648,8 +1573,8 @@ class GetFeedbackAnalytics(Resource): for entry in feedback_data: daily_feedback[entry["_id"]] = { - "positive": entry["likes"], - "negative": entry["dislikes"], + "positive": entry["positive"], + "negative": entry["negative"] } except Exception as err: @@ -2105,4 +2030,4 @@ class DeleteTool(Resource): except Exception as err: return {"success": False, "error": str(err)}, 400 - return {"success": True}, 200 \ No newline at end of file + return {"success": True}, 200 diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 8502cf36..35058be0 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -53,6 +53,7 @@ "default": "Default" }, "documents": { + "title": "This table contains all the documents that are available to you and those you have uploaded", "label": "Documents", "name": "Document Name", "date": "Vector Date", @@ -70,7 +71,8 @@ "weekly": "Weekly", "monthly": "Monthly" }, - "actions": "Actions" + "actions": "Actions", + "deleteWarning": "Are you sure you want to delete \"{{name}}\"?" }, "apiKeys": { "label": "Chatbots", @@ -78,7 +80,8 @@ "key": "API Key", "sourceDoc": "Source Document", "createNew": "Create New", - "noData": "No existing Chatbots" + "noData": "No existing Chatbots", + "deleteConfirmation": "Are you sure you want to delete the API key '{{name}}'?" }, "analytics": { "label": "Analytics", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index c5b0fa17..d0b47874 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -53,6 +53,7 @@ "default": "Predeterminado" }, "documents": { + "title": "Esta tabla contiene todos los documentos que están disponibles para ti y los que has subido", "label": "Documentos", "name": "Nombre del Documento", "date": "Fecha de Vector", @@ -70,7 +71,8 @@ "weekly": "Semanal", "monthly": "Mensual" }, - "actions": "Acciones" + "actions": "Acciones", + "deleteWarning": "¿Estás seguro de que deseas eliminar \"{{name}}\"?" }, "apiKeys": { "label": "Chatbots", @@ -78,7 +80,8 @@ "key": "Clave de API", "sourceDoc": "Documento Fuente", "createNew": "Crear Nuevo", - "noData": "No hay chatbots existentes" + "noData": "No hay chatbots existentes", + "deleteConfirmation": "¿Estás seguro de que quieres eliminar la clave API '{{name}}'?" }, "analytics": { "label": "Analítica", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 0098807a..2fdc2c61 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -70,15 +70,17 @@ "weekly": "毎週", "monthly": "毎月" }, - "actions": "アクション" + "actions": "アクション", + "deleteWarning": "\"{{name}}\"を削除してもよろしいですか?" }, "apiKeys": { - "label": "APIキー", + "label": "チャットボット", "name": "名前", "key": "APIキー", "sourceDoc": "ソースドキュメント", "createNew": "新規作成", - "noData": "既存のAPIキーがありません" + "noData": "既存のチャットボットはありません", + "deleteConfirmation": "APIキー '{{name}}' を削除してもよろしいですか?" }, "analytics": { "label": "分析", diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 69897452..087b8dfd 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -53,6 +53,7 @@ "default": "По умолчанию" }, "documents": { + "title": "Эта таблица содержит все документы, которые доступны вам и те, которые вы загрузили", "label": "Документы", "name": "Название документа", "date": "Дата вектора", @@ -70,7 +71,8 @@ "weekly": "Еженедельно", "monthly": "Ежемесячно" }, - "actions": "Действия" + "actions": "Действия", + "deleteWarning": "Вы уверены, что хотите удалить \"{{name}}\"?" }, "apiKeys": { "label": "API ключи", @@ -78,7 +80,8 @@ "key": "API ключ", "sourceDoc": "Источник документа", "createNew": "Создать новый", - "noData": "Нет существующих API ключей" + "noData": "Нет существующих чатботов", + "deleteConfirmation": "Вы уверены, что хотите удалить API ключ '{{name}}'?" }, "analytics": { "label": "Аналитика", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 3699afde..f2abf1e5 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -53,6 +53,7 @@ "default": "預設" }, "documents": { + "title": "此表格包含所有可供您使用的文件以及您上傳的文件", "label": "文件", "name": "文件名稱", "date": "向量日期", @@ -70,15 +71,17 @@ "weekly": "每週", "monthly": "每月" }, - "actions": "操作" + "actions": "操作", + "deleteWarning": "您確定要刪除 \"{{name}}\" 嗎?" }, "apiKeys": { "label": "聊天機器人", "name": "名稱", "key": "API 金鑰", "sourceDoc": "來源文件", - "createNew": "新增", - "noData": "沒有現有的聊天機器人" + "createNew": "建立新的", + "noData": "沒有現有的聊天機器人", + "deleteConfirmation": "您確定要刪除 API 金鑰 '{{name}}' 嗎?" }, "analytics": { "label": "分析", diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 9936350d..56c8fbb1 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -53,7 +53,8 @@ "default": "默认" }, "documents": { - "label": "文件", + "title": "此表格包含所有可供您使用的文档以及您上传的文档", + "label": "文档", "name": "文件名称", "date": "向量日期", "type": "类型", @@ -70,7 +71,8 @@ "weekly": "每周", "monthly": "每月" }, - "actions": "操作" + "actions": "操作", + "deleteWarning": "您确定要删除 \"{{name}}\" 吗?" }, "apiKeys": { "label": "聊天机器人", @@ -78,7 +80,8 @@ "key": "API 密钥", "sourceDoc": "源文档", "createNew": "创建新的", - "noData": "没有现有的聊天机器人" + "noData": "没有现有的聊天机器人", + "deleteConfirmation": "您确定要删除 API 密钥 '{{name}}' 吗?" }, "analytics": { "label": "分析", @@ -111,7 +114,7 @@ "searchPlaceholder": "搜索...", "addTool": "添加工具", "noToolsFound": "未找到工具", - "selectToolSetup": "选择要设置的工具" , + "selectToolSetup": "选择要设置的工具", "settingsIconAlt": "设置图标", "configureToolAria": "配置 {toolName}", "toggleToolAria": "切换 {toolName}" diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index 038e4bbb..44242cda 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -5,6 +5,7 @@ import userService from '../api/services/userService'; import Trash from '../assets/trash.svg'; import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; +import ConfirmationModal from '../modals/ConfirmationModal'; import { APIKeyData } from './types'; import SkeletonLoader from '../components/SkeletonLoader'; @@ -15,6 +16,10 @@ export default function APIKeys() { const [newKey, setNewKey] = React.useState(''); const [apiKeys, setApiKeys] = React.useState([]); const [loading, setLoading] = useState(true); + const [keyToDelete, setKeyToDelete] = useState<{ + id: string; + name: string; + } | null>(null); const handleFetchKeys = async () => { setLoading(true); @@ -44,6 +49,7 @@ export default function APIKeys() { .then((data) => { data.success === true && setApiKeys((previous) => previous.filter((elem) => elem.id !== id)); + setKeyToDelete(null); }) .catch((error) => { console.error(error); @@ -104,6 +110,18 @@ export default function APIKeys() { close={() => setSaveKeyModal(false)} /> )} + {keyToDelete && ( + setKeyToDelete(null)} + submitLabel={t('modals.deleteConv.delete')} + handleSubmit={() => handleDeleteKey(keyToDelete.id)} + handleCancel={() => setKeyToDelete(null)} + /> + )}
{loading ? ( @@ -157,7 +175,12 @@ export default function APIKeys() { alt={`Delete ${element.name}`} className="h-4 w-4 cursor-pointer hover:opacity-50" id={`img-${index}`} - onClick={() => handleDeleteKey(element.id)} + onClick={() => + setKeyToDelete({ + id: element.id, + name: element.name, + }) + } /> diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index ee04d121..88590165 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -15,7 +15,8 @@ import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure Act import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; import { setSourceDocs } from '../preferences/preferenceSlice'; import { setPaginatedDocuments } from '../preferences/preferenceSlice'; -import { truncate } from '../utils/stringUtils'; +import { formatDate } from '../utils/dateTimeUtils'; +import ConfirmationModal from '../modals/ConfirmationModal'; // Utility function to format numbers const formatTokens = (tokens: number): string => { @@ -85,6 +86,7 @@ const Documents: React.FC = ({ setSortField(newSortField); setSortOrder(newSortOrder); } + setLoading(true); getDocsWithPagination( newSortField, @@ -110,7 +112,6 @@ const Documents: React.FC = ({ userService .manageSync({ source_id: doc.id, sync_frequency }) .then(() => { - // First, fetch the updated source docs return getDocs(); }) .then((data) => { @@ -134,177 +135,184 @@ const Documents: React.FC = ({ }); }; - useEffect(() => { - if (modalState === 'INACTIVE') { - refreshDocs(sortField, currentPage, rowsPerPage); + const [documentToDelete, setDocumentToDelete] = useState<{ + index: number; + document: Doc; + } | null>(null); + const [deleteModalState, setDeleteModalState] = + useState('INACTIVE'); + + const handleDeleteConfirmation = (index: number, document: Doc) => { + setDocumentToDelete({ index, document }); + setDeleteModalState('ACTIVE'); + }; + + const handleConfirmedDelete = () => { + if (documentToDelete) { + handleDeleteDocument(documentToDelete.index, documentToDelete.document); + setDeleteModalState('INACTIVE'); + setDocumentToDelete(null); } - }, [modalState]); + }; useEffect(() => { - // undefine to prevent reset the sort order refreshDocs(undefined, 1, rowsPerPage); }, [searchTerm]); return ( -
-
-
-
-
- - { - setSearchTerm(e.target.value); - setCurrentPage(1); - }} - /> -
- -
- {loading ? ( - - ) : ( -
-
-
- - - - - - - {/*} - - */} - - - - - {!currentDocuments?.length && ( - - - - )} - {Array.isArray(currentDocuments) && - currentDocuments.map((document, index) => ( - - - - - {/*} - - */} - - - ))} - -
- {t('settings.documents.name')} - -
- {t('settings.documents.date')} - refreshDocs('date')} - src={caretSort} - alt="sort" - /> -
-
-
- {t('settings.documents.tokenUsage')} - refreshDocs('tokens')} - src={caretSort} - alt="sort" - /> -
-
-
- {t('settings.documents.type')} -
-
- {t('settings.documents.actions')} -
- {t('settings.documents.noData')} -
- {truncate(document.name, 50)} - - {document.date} - - {document.tokens - ? formatTokens(+document.tokens) - : ''} - - {document.type === 'remote' - ? 'Pre-loaded' - : 'Private'} - -
- {document.type !== 'remote' && ( - {t('convTile.delete')} { - event.stopPropagation(); - handleDeleteDocument(index, document); - }} - /> - )} - {document.syncFrequency && ( -
- { - handleManageSync(document, value); - }} - defaultValue={document.syncFrequency} - icon={SyncIcon} - /> -
- )} -
-
-
-
-
- )} +
+
+
+

+ {t('settings.documents.title')} +

- {/* outside scrollable area */} +
+
+ + { + setSearchTerm(e.target.value); + setCurrentPage(1); + }} + /> +
+ +
+ + {loading ? ( + + ) : ( +
+ {' '} + {/* Removed overflow-auto */} +
+ + + + + + + + + + + {!currentDocuments?.length ? ( + + + + ) : ( + currentDocuments.map((document, index) => ( + + + + + + + )) + )} + +
+ {t('settings.documents.name')} + +
+ {t('settings.documents.date')} + refreshDocs('date')} + src={caretSort} + alt="sort" + /> +
+
+
+ + {t('settings.documents.tokenUsage')} + + + {t('settings.documents.tokenUsage')} + + refreshDocs('tokens')} + src={caretSort} + alt="sort" + /> +
+
+ + {t('settings.documents.actions')} + +
+ {t('settings.documents.noData')} +
+ {document.name} + + {document.date ? formatDate(document.date) : ''} + + {document.tokens + ? formatTokens(+document.tokens) + : ''} + +
+ {!document.syncFrequency && ( +
+ )} + {document.syncFrequency && ( + { + handleManageSync(document, value); + }} + defaultValue={document.syncFrequency} + icon={SyncIcon} + /> + )} + +
+
+
+
+ )} +
+ +
= ({ refreshDocs(undefined, 1, rows); }} /> - - {/* Conditionally render the Upload modal based on modalState */} - {modalState === 'ACTIVE' && ( -
-
- {/* Your Upload component */} - setModalState('INACTIVE')} - /> -
-
- )}
+ + {modalState === 'ACTIVE' && ( + setModalState('INACTIVE')} + onSuccessfulUpload={() => + refreshDocs(undefined, currentPage, rowsPerPage) + } + /> + )} + + {deleteModalState === 'ACTIVE' && documentToDelete && ( + { + setDeleteModalState('INACTIVE'); + setDocumentToDelete(null); + }} + submitLabel={t('convTile.delete')} + /> + )}
); }; diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 59a2bf93..b1c45156 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -23,12 +23,14 @@ function Upload({ isOnboarding, renderTab = null, close, + onSuccessfulUpload = () => undefined, }: { receivedFile: File[]; setModalState: (state: ActiveState) => void; isOnboarding: boolean; renderTab: string | null; close: () => void; + onSuccessfulUpload?: () => void; }) { const [docName, setDocName] = useState(receivedFile[0]?.name); const [urlName, setUrlName] = useState(''); @@ -218,6 +220,7 @@ function Upload({ setfiles([]); setProgress(undefined); setModalState('INACTIVE'); + onSuccessfulUpload?.(); } } else if (data.status == 'PROGRESS') { setProgress( @@ -424,20 +427,26 @@ function Upload({

{t('modals.uploadDoc.info')}

-
+

{t('modals.uploadDoc.uploadedFiles')}

- {files.map((file) => ( -

- {file.name} -

- ))} - {files.length === 0 && ( -

- {t('none')} -

- )} +
+ {files.map((file) => ( +

+ {file.name} +

+ ))} + {files.length === 0 && ( +

+ {t('none')} +

+ )} +
)} @@ -597,47 +606,49 @@ function Upload({ {t('modals.uploadDoc.back')} )} - + className={`rounded-3xl px-4 py-2 font-medium ${ + (activeTab === 'file' && (!files.length || !docName)) || + (activeTab === 'remote' && + ((urlType.label !== 'Reddit' && + urlType.label !== 'GitHub' && + (!url || !urlName)) || + (urlType.label === 'GitHub' && !repoUrl) || + (urlType.label === 'Reddit' && + (!redditData.client_id || + !redditData.client_secret || + !redditData.user_agent || + !redditData.search_queries || + !redditData.number_posts)))) + ? 'cursor-not-allowed bg-gray-300 text-gray-500' + : 'cursor-pointer bg-purple-30 text-white hover:bg-purple-40' + }`} + > + {t('modals.uploadDoc.train')} + + )}
); diff --git a/frontend/src/utils/dateTimeUtils.ts b/frontend/src/utils/dateTimeUtils.ts index 7f89007c..7815f123 100644 --- a/frontend/src/utils/dateTimeUtils.ts +++ b/frontend/src/utils/dateTimeUtils.ts @@ -1,20 +1,39 @@ export function formatDate(dateString: string): string { - if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) { - const dateTime = new Date(dateString); - return dateTime.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(dateString)) { - const dateTime = new Date(dateString); - return dateTime.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + try { const date = new Date(dateString); - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - } else { + + if (isNaN(date.getTime())) { + throw new Error('Invalid date'); + } + + const userLocale = navigator.language || 'en-US'; + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + const weekday = date.toLocaleDateString(userLocale, { + weekday: 'short', + timeZone: userTimezone, + }); + + const monthDay = date.toLocaleDateString(userLocale, { + day: '2-digit', + month: 'short', + year: 'numeric', + timeZone: userTimezone, + }); + + const time = date + .toLocaleTimeString(userLocale, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: true, + timeZone: userTimezone, + }) + .replace(/am|pm/i, (match) => match.toUpperCase()); + + return `${weekday}, ${monthDay} ${time}`; + } catch (error) { + console.error('Error formatting date:', error); return dateString; } }