mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge pull request #1607 from ManishMadan2882/main
Perfecting Settings/Documents
This commit is contained in:
@@ -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
|
||||
return {"success": True}, 200
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "分析",
|
||||
|
||||
@@ -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": "Аналитика",
|
||||
|
||||
@@ -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": "分析",
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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<APIKeyData[]>([]);
|
||||
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 && (
|
||||
<ConfirmationModal
|
||||
message={t('settings.apiKeys.deleteConfirmation', {
|
||||
name: keyToDelete.name,
|
||||
})}
|
||||
modalState="ACTIVE"
|
||||
setModalState={() => setKeyToDelete(null)}
|
||||
submitLabel={t('modals.deleteConv.delete')}
|
||||
handleSubmit={() => handleDeleteKey(keyToDelete.id)}
|
||||
handleCancel={() => setKeyToDelete(null)}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-[27px] w-full">
|
||||
<div className="w-full overflow-x-auto">
|
||||
{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,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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<DocumentsProps> = ({
|
||||
setSortField(newSortField);
|
||||
setSortOrder(newSortOrder);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
getDocsWithPagination(
|
||||
newSortField,
|
||||
@@ -110,7 +112,6 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
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<DocumentsProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (modalState === 'INACTIVE') {
|
||||
refreshDocs(sortField, currentPage, rowsPerPage);
|
||||
const [documentToDelete, setDocumentToDelete] = useState<{
|
||||
index: number;
|
||||
document: Doc;
|
||||
} | null>(null);
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
useState<ActiveState>('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 (
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col relative">
|
||||
<div className="z-10 w-full overflow-x-auto">
|
||||
<div className="my-3 flex justify-between items-center">
|
||||
<div className="p-1">
|
||||
<label htmlFor="document-search-input" className="sr-only">
|
||||
{t('settings.documents.searchPlaceholder')}
|
||||
</label>
|
||||
<Input
|
||||
maxLength={256}
|
||||
placeholder={t('settings.documents.searchPlaceholder')}
|
||||
name="Document-search-input"
|
||||
type="text"
|
||||
id="document-search-input"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
||||
title={t('settings.documents.addNew')}
|
||||
onClick={() => {
|
||||
setIsOnboarding(false);
|
||||
setModalState('ACTIVE');
|
||||
}}
|
||||
>
|
||||
{t('settings.documents.addNew')}
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} />
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex-grow">
|
||||
<div className="dark:border-silver/40 border-silver rounded-md border overflow-auto">
|
||||
<table className="min-w-full divide-y divide-silver dark:divide-silver/40 text-xs sm:text-sm ">
|
||||
<thead>
|
||||
<tr className="text-nowrap">
|
||||
<th className="px-5 py-3 text-start font-medium text-gray-700 dark:text-gray-50 uppercase w-96">
|
||||
{t('settings.documents.name')}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-start font-medium text-gray-700 dark:text-gray-50 uppercase">
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.date')}
|
||||
<img
|
||||
className="cursor-pointer"
|
||||
onClick={() => refreshDocs('date')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-5 py-2 text-center font-medium text-gray-700 dark:text-gray-50 uppercase"
|
||||
>
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.tokenUsage')}
|
||||
<img
|
||||
className="cursor-pointer"
|
||||
onClick={() => refreshDocs('tokens')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
{/*}
|
||||
<th className="px-5 py-2 text-start text-sm font-medium text-gray-700 dark:text-gray-50 uppercase">
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.type')}
|
||||
</div>
|
||||
</th>
|
||||
*/}
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-2 text-start font-medium text-gray-700 dark:text-gray-50 uppercase sr-only"
|
||||
aria-label={t('settings.documents.actions')}
|
||||
>
|
||||
{t('settings.documents.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{!currentDocuments?.length && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="!py-4 text-gray-800 dark:text-neutral-200 text-center"
|
||||
>
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{Array.isArray(currentDocuments) &&
|
||||
currentDocuments.map((document, index) => (
|
||||
<tr key={index} className="text-nowrap font-normal">
|
||||
<td
|
||||
title={document.name}
|
||||
className="px-6 py-4 whitespace-nowrap text-left font-medium text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{truncate(document.name, 50)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center font-medium text-gray-800 dark:text-neutral-200">
|
||||
{document.date}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center font-medium text-gray-800 dark:text-neutral-200">
|
||||
{document.tokens
|
||||
? formatTokens(+document.tokens)
|
||||
: ''}
|
||||
</td>
|
||||
{/*}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center text-sm font-medium text-gray-800 dark:text-neutral-200">
|
||||
{document.type === 'remote'
|
||||
? 'Pre-loaded'
|
||||
: 'Private'}
|
||||
</td>
|
||||
*/}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-left text-sm font-medium flex">
|
||||
<div className="min-w-[150px] flex flex-row items-center ml-auto gap-10">
|
||||
{document.type !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt={t('convTile.delete')}
|
||||
className="h-4 w-4 cursor-pointer opacity-60 hover:opacity-100"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<div className="ml-2">
|
||||
<DropdownMenu
|
||||
name={t('settings.documents.sync')}
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col mt-8">
|
||||
<div className="flex flex-col relative flex-grow">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-base font-medium text-sonic-silver">
|
||||
{t('settings.documents.title')}
|
||||
</h2>
|
||||
</div>
|
||||
{/* outside scrollable area */}
|
||||
<div className="my-3 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
||||
<div className="w-full sm:w-auto">
|
||||
<label htmlFor="document-search-input" className="sr-only">
|
||||
{t('settings.documents.searchPlaceholder')}
|
||||
</label>
|
||||
<Input
|
||||
maxLength={256}
|
||||
placeholder={t('settings.documents.searchPlaceholder')}
|
||||
name="Document-search-input"
|
||||
type="text"
|
||||
id="document-search-input"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
||||
title={t('settings.documents.addNew')}
|
||||
onClick={() => {
|
||||
setIsOnboarding(false);
|
||||
setModalState('ACTIVE');
|
||||
}}
|
||||
>
|
||||
{t('settings.documents.addNew')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<SkeletonLoader count={1} />
|
||||
) : (
|
||||
<div className="flex flex-col flex-grow">
|
||||
{' '}
|
||||
{/* Removed overflow-auto */}
|
||||
<div className="border rounded-md border-gray-300 dark:border-silver/40 overflow-hidden">
|
||||
<table className="w-full min-w-[640px] table-auto">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-300 dark:border-silver/40">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[45%]">
|
||||
{t('settings.documents.name')}
|
||||
</th>
|
||||
<th className="py-3 px-4 text-center text-xs font-medium text-sonic-silver uppercase w-[20%]">
|
||||
<div className="flex justify-center items-center">
|
||||
{t('settings.documents.date')}
|
||||
<img
|
||||
className="cursor-pointer ml-2"
|
||||
onClick={() => refreshDocs('date')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th className="py-3 px-4 text-center text-xs font-medium text-sonic-silver uppercase w-[25%]">
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="hidden sm:inline">
|
||||
{t('settings.documents.tokenUsage')}
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
{t('settings.documents.tokenUsage')}
|
||||
</span>
|
||||
<img
|
||||
className="cursor-pointer ml-2"
|
||||
onClick={() => refreshDocs('tokens')}
|
||||
src={caretSort}
|
||||
alt="sort"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] uppercase w-[10%]">
|
||||
<span className="sr-only">
|
||||
{t('settings.documents.actions')}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-300 dark:divide-silver/40">
|
||||
{!currentDocuments?.length ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="py-4 text-center text-gray-700 dark:text-[#E0E0E] bg-transparent"
|
||||
>
|
||||
{t('settings.documents.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
currentDocuments.map((document, index) => (
|
||||
<tr key={index} className="group transition-colors">
|
||||
<td
|
||||
className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] w-[45%] truncate group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||
title={document.name}
|
||||
>
|
||||
{document.name}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-center text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap w-[20%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
{document.date ? formatDate(document.date) : ''}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-center text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap w-[25%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
{document.tokens
|
||||
? formatTokens(+document.tokens)
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-right w-[10%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{!document.syncFrequency && (
|
||||
<div className="w-8"></div>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<DropdownMenu
|
||||
name={t('settings.documents.sync')}
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteConfirmation(index, document);
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0"
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt={t('convTile.delete')}
|
||||
className="h-4 w-4 opacity-60 hover:opacity-100"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-4">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
@@ -319,23 +327,36 @@ const Documents: React.FC<DocumentsProps> = ({
|
||||
refreshDocs(undefined, 1, rows);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Conditionally render the Upload modal based on modalState */}
|
||||
{modalState === 'ACTIVE' && (
|
||||
<div className="fixed top-0 left-0 w-screen h-screen z-50 flex items-center justify-center bg-transparent">
|
||||
<div className="w-full h-full bg-transparent flex flex-col items-center justify-center p-8">
|
||||
{/* Your Upload component */}
|
||||
<Upload
|
||||
receivedFile={[]}
|
||||
setModalState={setModalState}
|
||||
isOnboarding={isOnboarding}
|
||||
renderTab={null}
|
||||
close={() => setModalState('INACTIVE')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{modalState === 'ACTIVE' && (
|
||||
<Upload
|
||||
receivedFile={[]}
|
||||
setModalState={setModalState}
|
||||
isOnboarding={isOnboarding}
|
||||
renderTab={null}
|
||||
close={() => setModalState('INACTIVE')}
|
||||
onSuccessfulUpload={() =>
|
||||
refreshDocs(undefined, currentPage, rowsPerPage)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteModalState === 'ACTIVE' && documentToDelete && (
|
||||
<ConfirmationModal
|
||||
message={t('settings.documents.deleteWarning', {
|
||||
name: documentToDelete.document.name,
|
||||
})}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={handleConfirmedDelete}
|
||||
handleCancel={() => {
|
||||
setDeleteModalState('INACTIVE');
|
||||
setDocumentToDelete(null);
|
||||
}}
|
||||
submitLabel={t('convTile.delete')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
<p className="mb-0 text-xs italic text-gray-4000">
|
||||
{t('modals.uploadDoc.info')}
|
||||
</p>
|
||||
<div className="mt-0">
|
||||
<div className="mt-0 max-w-full">
|
||||
<p className="mb-[14px] font-medium text-eerie-black dark:text-light-gray">
|
||||
{t('modals.uploadDoc.uploadedFiles')}
|
||||
</p>
|
||||
{files.map((file) => (
|
||||
<p key={file.name} className="text-gray-6000">
|
||||
{file.name}
|
||||
</p>
|
||||
))}
|
||||
{files.length === 0 && (
|
||||
<p className="text-gray-6000 dark:text-light-gray">
|
||||
{t('none')}
|
||||
</p>
|
||||
)}
|
||||
<div className="max-w-full overflow-hidden">
|
||||
{files.map((file) => (
|
||||
<p
|
||||
key={file.name}
|
||||
className="text-gray-6000 truncate overflow-hidden text-ellipsis"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</p>
|
||||
))}
|
||||
{files.length === 0 && (
|
||||
<p className="text-gray-6000 dark:text-light-gray">
|
||||
{t('none')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -597,47 +606,49 @@ function Upload({
|
||||
{t('modals.uploadDoc.back')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (activeTab === 'file') {
|
||||
uploadFile();
|
||||
} else {
|
||||
uploadRemote();
|
||||
{activeTab && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (activeTab === 'file') {
|
||||
uploadFile();
|
||||
} else {
|
||||
uploadRemote();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
(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))))
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
(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))))
|
||||
}
|
||||
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')}
|
||||
</button>
|
||||
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')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user