-
Selected Files
+
+ {t('modals.uploadDoc.connectors.googleDrive.selectedFiles')}
+
{selectedFiles.length === 0 && selectedFolders.length === 0 ? (
- No files or folders selected
+ {t(
+ 'modals.uploadDoc.connectors.googleDrive.noFilesSelected',
+ )}
) : (
{selectedFolders.length > 0 && (
- Folders
+ {t('modals.uploadDoc.connectors.googleDrive.folders')}
{selectedFolders.map((folder) => (
= ({
>
@@ -337,7 +355,9 @@ const GoogleDrivePicker: React.FC = ({
}}
className="ml-2 text-sm text-red-500 hover:text-red-700"
>
- Remove
+ {t(
+ 'modals.uploadDoc.connectors.googleDrive.remove',
+ )}
))}
@@ -347,7 +367,7 @@ const GoogleDrivePicker: React.FC
= ({
{selectedFiles.length > 0 && (
- Files
+ {t('modals.uploadDoc.connectors.googleDrive.files')}
{selectedFiles.map((file) => (
= ({
>
@@ -375,7 +397,9 @@ const GoogleDrivePicker: React.FC = ({
}}
className="ml-2 text-sm text-red-500 hover:text-red-700"
>
- Remove
+ {t(
+ 'modals.uploadDoc.connectors.googleDrive.remove',
+ )}
))}
diff --git a/frontend/src/components/UploadToast.tsx b/frontend/src/components/UploadToast.tsx
new file mode 100644
index 00000000..dbd23f64
--- /dev/null
+++ b/frontend/src/components/UploadToast.tsx
@@ -0,0 +1,229 @@
+import { useState } from 'react';
+
+import { useDispatch, useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { selectUploadTasks, dismissUploadTask } from '../upload/uploadSlice';
+import ChevronDown from '../assets/chevron-down.svg';
+import CheckCircleFilled from '../assets/check-circle-filled.svg';
+import WarnIcon from '../assets/warn.svg';
+
+const PROGRESS_RADIUS = 10;
+const PROGRESS_CIRCUMFERENCE = 2 * Math.PI * PROGRESS_RADIUS;
+
+export default function UploadToast() {
+ const [collapsedTasks, setCollapsedTasks] = useState
>(
+ {},
+ );
+
+ const toggleTaskCollapse = (taskId: string) => {
+ setCollapsedTasks((prev) => ({
+ ...prev,
+ [taskId]: !prev[taskId],
+ }));
+ };
+
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const uploadTasks = useSelector(selectUploadTasks);
+
+ const getStatusHeading = (status: string) => {
+ switch (status) {
+ case 'preparing':
+ return t('modals.uploadDoc.progress.wait');
+ case 'uploading':
+ return t('modals.uploadDoc.progress.upload');
+ case 'training':
+ return t('modals.uploadDoc.progress.upload');
+ case 'completed':
+ return t('modals.uploadDoc.progress.completed');
+ case 'failed':
+ return t('attachments.uploadFailed');
+ default:
+ return t('modals.uploadDoc.progress.preparing');
+ }
+ };
+
+ return (
+
+ {uploadTasks
+ .filter((task) => !task.dismissed)
+ .map((task) => {
+ const shouldShowProgress = [
+ 'preparing',
+ 'uploading',
+ 'training',
+ ].includes(task.status);
+ const rawProgress = Math.min(Math.max(task.progress ?? 0, 0), 100);
+ const formattedProgress = Math.round(rawProgress);
+ const progressOffset =
+ PROGRESS_CIRCUMFERENCE * (1 - rawProgress / 100);
+ const isCollapsed = collapsedTasks[task.id] ?? false;
+
+ return (
+
+
+
+
+ {getStatusHeading(task.status)}
+
+
+
+
+
+
+
+
+
+
+
+ {task.fileName}
+
+
+
+ {shouldShowProgress && (
+
+ )}
+
+ {task.status === 'completed' && (
+

+ )}
+
+ {task.status === 'failed' && (
+

+ )}
+
+
+
+ {task.status === 'failed' && task.errorMessage && (
+
+ {task.errorMessage}
+
+ )}
+
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
index 35419534..25a8fb9e 100644
--- a/frontend/src/locale/en.json
+++ b/frontend/src/locale/en.json
@@ -229,6 +229,9 @@
"uploadDoc": {
"label": "Upload new document",
"select": "Choose how to upload your document to DocsGPT",
+ "selectSource": "Select the way to add your source",
+ "selectedFiles": "Selected Files",
+ "noFilesSelected": "No files selected",
"file": "Upload from device",
"back": "Back",
"wait": "Please wait ...",
@@ -257,13 +260,74 @@
},
"progress": {
"upload": "Upload is in progress",
- "training": "Training is in progress",
- "completed": "Training completed",
+ "training": "Upload is in progress",
+ "completed": "Upload completed",
"wait": "This may take several minutes",
- "tokenLimit": "Over the token limit, please consider uploading smaller document"
+ "preparing": "Preparing upload",
+ "tokenLimit": "Over the token limit, please consider uploading smaller document",
+ "expandDetails": "Expand upload details",
+ "collapseDetails": "Collapse upload details",
+ "dismiss": "Dismiss upload toast",
+ "uploadProgress": "Upload progress {{progress}}%",
+ "clear": "Clear"
},
"showAdvanced": "Show advanced options",
- "hideAdvanced": "Hide advanced options"
+ "hideAdvanced": "Hide advanced options",
+ "ingestors": {
+ "local_file": {
+ "label": "Upload File",
+ "heading": "Upload new document"
+ },
+ "crawler": {
+ "label": "Crawler",
+ "heading": "Add content with Web Crawler"
+ },
+ "url": {
+ "label": "Link",
+ "heading": "Add content from URL"
+ },
+ "github": {
+ "label": "GitHub",
+ "heading": "Add content from GitHub"
+ },
+ "reddit": {
+ "label": "Reddit",
+ "heading": "Add content from Reddit"
+ },
+ "google_drive": {
+ "label": "Google Drive",
+ "heading": "Upload from Google Drive"
+ }
+ },
+ "connectors": {
+ "auth": {
+ "connectedUser": "Connected User",
+ "authFailed": "Authentication failed",
+ "authUrlFailed": "Failed to get authorization URL",
+ "popupBlocked": "Failed to open authentication window. Please allow popups.",
+ "authCancelled": "Authentication was cancelled",
+ "connectedAs": "Connected as {{email}}",
+ "disconnect": "Disconnect"
+ },
+ "googleDrive": {
+ "connect": "Connect to Google Drive",
+ "sessionExpired": "Session expired. Please reconnect to Google Drive.",
+ "sessionExpiredGeneric": "Session expired. Please reconnect your account.",
+ "validateFailed": "Failed to validate session. Please reconnect.",
+ "noSession": "No valid session found. Please reconnect to Google Drive.",
+ "noAccessToken": "No access token available. Please reconnect to Google Drive.",
+ "pickerFailed": "Failed to open file picker. Please try again.",
+ "selectedFiles": "Selected Files",
+ "selectFiles": "Select Files",
+ "loading": "Loading...",
+ "noFilesSelected": "No files or folders selected",
+ "folders": "Folders",
+ "files": "Files",
+ "remove": "Remove",
+ "folderAlt": "Folder",
+ "fileAlt": "File"
+ }
+ }
},
"createAPIKey": {
"label": "Create New API Key",
diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json
index 4fb761f5..cedc3232 100644
--- a/frontend/src/locale/es.json
+++ b/frontend/src/locale/es.json
@@ -192,6 +192,9 @@
"uploadDoc": {
"label": "Subir nuevo documento",
"select": "Elige cómo cargar tu documento en DocsGPT",
+ "selectSource": "Selecciona la forma de agregar tu fuente",
+ "selectedFiles": "Archivos Seleccionados",
+ "noFilesSelected": "No hay archivos seleccionados",
"file": "Subir desde el dispositivo",
"back": "Atrás",
"wait": "Por favor espera ...",
@@ -220,13 +223,74 @@
},
"progress": {
"upload": "Subida en progreso",
- "training": "Entrenamiento en progreso",
- "completed": "Entrenamiento completado",
+ "training": "Subida en progreso",
+ "completed": "Subida completada",
"wait": "Esto puede tardar varios minutos",
- "tokenLimit": "Excede el límite de tokens, considere cargar un documento más pequeño"
+ "preparing": "Preparando subida",
+ "tokenLimit": "Excede el límite de tokens, considere cargar un documento más pequeño",
+ "expandDetails": "Expandir detalles de subida",
+ "collapseDetails": "Contraer detalles de subida",
+ "dismiss": "Descartar notificación de subida",
+ "uploadProgress": "Progreso de subida {{progress}}%",
+ "clear": "Limpiar"
},
"showAdvanced": "Mostrar opciones avanzadas",
- "hideAdvanced": "Ocultar opciones avanzadas"
+ "hideAdvanced": "Ocultar opciones avanzadas",
+ "ingestors": {
+ "local_file": {
+ "label": "Subir archivo",
+ "heading": "Subir nuevo documento"
+ },
+ "crawler": {
+ "label": "Rastreador",
+ "heading": "Agregar contenido con rastreador web"
+ },
+ "url": {
+ "label": "Enlace",
+ "heading": "Agregar contenido desde URL"
+ },
+ "github": {
+ "label": "GitHub",
+ "heading": "Agregar contenido desde GitHub"
+ },
+ "reddit": {
+ "label": "Reddit",
+ "heading": "Agregar contenido desde Reddit"
+ },
+ "google_drive": {
+ "label": "Google Drive",
+ "heading": "Subir desde Google Drive"
+ }
+ },
+ "connectors": {
+ "auth": {
+ "connectedUser": "Usuario Conectado",
+ "authFailed": "Autenticación fallida",
+ "authUrlFailed": "Error al obtener la URL de autorización",
+ "popupBlocked": "Error al abrir la ventana de autenticación. Por favor, permita ventanas emergentes.",
+ "authCancelled": "Autenticación cancelada",
+ "connectedAs": "Conectado como {{email}}",
+ "disconnect": "Desconectar"
+ },
+ "googleDrive": {
+ "connect": "Conectar a Google Drive",
+ "sessionExpired": "Sesión expirada. Por favor, reconecte a Google Drive.",
+ "sessionExpiredGeneric": "Sesión expirada. Por favor, reconecte su cuenta.",
+ "validateFailed": "Error al validar la sesión. Por favor, reconecte.",
+ "noSession": "No se encontró una sesión válida. Por favor, reconecte a Google Drive.",
+ "noAccessToken": "No hay token de acceso disponible. Por favor, reconecte a Google Drive.",
+ "pickerFailed": "Error al abrir el selector de archivos. Por favor, inténtelo de nuevo.",
+ "selectedFiles": "Archivos Seleccionados",
+ "selectFiles": "Seleccionar Archivos",
+ "loading": "Cargando...",
+ "noFilesSelected": "No hay archivos o carpetas seleccionados",
+ "folders": "Carpetas",
+ "files": "Archivos",
+ "remove": "Eliminar",
+ "folderAlt": "Carpeta",
+ "fileAlt": "Archivo"
+ }
+ }
},
"createAPIKey": {
"label": "Crear Nueva Clave de API",
diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json
index a805eecf..1072b17c 100644
--- a/frontend/src/locale/jp.json
+++ b/frontend/src/locale/jp.json
@@ -192,6 +192,9 @@
"uploadDoc": {
"label": "新しい文書をアップロードする",
"select": "ドキュメントを DocsGPT にアップロードする方法を選択します",
+ "selectSource": "ソースを追加する方法を選択してください",
+ "selectedFiles": "選択されたファイル",
+ "noFilesSelected": "ファイルが選択されていません",
"file": "デバイスからアップロード",
"back": "戻る",
"wait": "お待ちください ...",
@@ -220,13 +223,74 @@
},
"progress": {
"upload": "アップロード中",
- "training": "トレーニング中",
- "completed": "トレーニング完了",
+ "training": "アップロード中",
+ "completed": "アップロード完了",
"wait": "数分かかる場合があります",
- "tokenLimit": "トークン制限を超えています。より小さいドキュメントをアップロードしてください"
+ "preparing": "アップロードを準備中",
+ "tokenLimit": "トークン制限を超えています。より小さいドキュメントをアップロードしてください",
+ "expandDetails": "アップロードの詳細を展開",
+ "collapseDetails": "アップロードの詳細を折りたたむ",
+ "dismiss": "アップロード通知を閉じる",
+ "uploadProgress": "アップロード進行状況 {{progress}}%",
+ "clear": "クリア"
},
"showAdvanced": "詳細オプションを表示",
- "hideAdvanced": "詳細オプションを非表示"
+ "hideAdvanced": "詳細オプションを非表示",
+ "ingestors": {
+ "local_file": {
+ "label": "ファイルをアップロード",
+ "heading": "新しいドキュメントをアップロード"
+ },
+ "crawler": {
+ "label": "クローラー",
+ "heading": "Webクローラーでコンテンツを追加"
+ },
+ "url": {
+ "label": "リンク",
+ "heading": "URLからコンテンツを追加"
+ },
+ "github": {
+ "label": "GitHub",
+ "heading": "GitHubからコンテンツを追加"
+ },
+ "reddit": {
+ "label": "Reddit",
+ "heading": "Redditからコンテンツを追加"
+ },
+ "google_drive": {
+ "label": "Google Drive",
+ "heading": "Google Driveからアップロード"
+ }
+ },
+ "connectors": {
+ "auth": {
+ "connectedUser": "接続されたユーザー",
+ "authFailed": "認証に失敗しました",
+ "authUrlFailed": "認証URLの取得に失敗しました",
+ "popupBlocked": "認証ウィンドウを開けませんでした。ポップアップを許可してください。",
+ "authCancelled": "認証がキャンセルされました",
+ "connectedAs": "{{email}}として接続",
+ "disconnect": "切断"
+ },
+ "googleDrive": {
+ "connect": "Google Driveに接続",
+ "sessionExpired": "セッションが期限切れです。Google Driveに再接続してください。",
+ "sessionExpiredGeneric": "セッションが期限切れです。アカウントに再接続してください。",
+ "validateFailed": "セッションの検証に失敗しました。再接続してください。",
+ "noSession": "有効なセッションが見つかりません。Google Driveに再接続してください。",
+ "noAccessToken": "アクセストークンが利用できません。Google Driveに再接続してください。",
+ "pickerFailed": "ファイルピッカーを開けませんでした。もう一度お試しください。",
+ "selectedFiles": "選択されたファイル",
+ "selectFiles": "ファイルを選択",
+ "loading": "読み込み中...",
+ "noFilesSelected": "ファイルまたはフォルダが選択されていません",
+ "folders": "フォルダ",
+ "files": "ファイル",
+ "remove": "削除",
+ "folderAlt": "フォルダ",
+ "fileAlt": "ファイル"
+ }
+ }
},
"createAPIKey": {
"label": "新しいAPIキーを作成",
diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json
index f2b2111e..0ca63ec5 100644
--- a/frontend/src/locale/ru.json
+++ b/frontend/src/locale/ru.json
@@ -192,6 +192,9 @@
"uploadDoc": {
"label": "Загрузить новый документ",
"select": "Выберите способ загрузки документа в DocsGPT",
+ "selectSource": "Выберите способ добавления источника",
+ "selectedFiles": "Выбранные файлы",
+ "noFilesSelected": "Файлы не выбраны",
"file": "Загрузить с устройства",
"back": "Назад",
"wait": "Пожалуйста, подождите...",
@@ -220,13 +223,74 @@
},
"progress": {
"upload": "Идет загрузка",
- "training": "Идет обучение",
- "completed": "Обучение завершено",
+ "training": "Идет загрузка",
+ "completed": "Загрузка завершена",
"wait": "Это может занять несколько минут",
- "tokenLimit": "Превышен лимит токенов, рассмотрите возможность загрузки документа меньшего размера"
+ "preparing": "Подготовка загрузки",
+ "tokenLimit": "Превышен лимит токенов, рассмотрите возможность загрузки документа меньшего размера",
+ "expandDetails": "Развернуть детали загрузки",
+ "collapseDetails": "Свернуть детали загрузки",
+ "dismiss": "Закрыть уведомление о загрузке",
+ "uploadProgress": "Прогресс загрузки {{progress}}%",
+ "clear": "Очистить"
},
"showAdvanced": "Показать расширенные настройки",
- "hideAdvanced": "Скрыть расширенные настройки"
+ "hideAdvanced": "Скрыть расширенные настройки",
+ "ingestors": {
+ "local_file": {
+ "label": "Загрузить файл",
+ "heading": "Загрузить новый документ"
+ },
+ "crawler": {
+ "label": "Краулер",
+ "heading": "Добавить контент с помощью веб-краулера"
+ },
+ "url": {
+ "label": "Ссылка",
+ "heading": "Добавить контент из URL"
+ },
+ "github": {
+ "label": "GitHub",
+ "heading": "Добавить контент из GitHub"
+ },
+ "reddit": {
+ "label": "Reddit",
+ "heading": "Добавить контент из Reddit"
+ },
+ "google_drive": {
+ "label": "Google Drive",
+ "heading": "Загрузить из Google Drive"
+ }
+ },
+ "connectors": {
+ "auth": {
+ "connectedUser": "Подключенный пользователь",
+ "authFailed": "Ошибка аутентификации",
+ "authUrlFailed": "Не удалось получить URL авторизации",
+ "popupBlocked": "Не удалось открыть окно аутентификации. Пожалуйста, разрешите всплывающие окна.",
+ "authCancelled": "Аутентификация отменена",
+ "connectedAs": "Подключен как {{email}}",
+ "disconnect": "Отключить"
+ },
+ "googleDrive": {
+ "connect": "Подключиться к Google Drive",
+ "sessionExpired": "Сеанс истек. Пожалуйста, переподключитесь к Google Drive.",
+ "sessionExpiredGeneric": "Сеанс истек. Пожалуйста, переподключите свою учетную запись.",
+ "validateFailed": "Не удалось проверить сеанс. Пожалуйста, переподключитесь.",
+ "noSession": "Действительный сеанс не найден. Пожалуйста, переподключитесь к Google Drive.",
+ "noAccessToken": "Токен доступа недоступен. Пожалуйста, переподключитесь к Google Drive.",
+ "pickerFailed": "Не удалось открыть средство выбора файлов. Пожалуйста, попробуйте еще раз.",
+ "selectedFiles": "Выбранные файлы",
+ "selectFiles": "Выбрать файлы",
+ "loading": "Загрузка...",
+ "noFilesSelected": "Файлы или папки не выбраны",
+ "folders": "Папки",
+ "files": "Файлы",
+ "remove": "Удалить",
+ "folderAlt": "Папка",
+ "fileAlt": "Файл"
+ }
+ }
},
"createAPIKey": {
"label": "Создать новый API ключ",
diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json
index a4247cf1..60a0c91d 100644
--- a/frontend/src/locale/zh-TW.json
+++ b/frontend/src/locale/zh-TW.json
@@ -192,6 +192,9 @@
"uploadDoc": {
"label": "上傳新文件",
"select": "選擇如何將文件上傳到 DocsGPT",
+ "selectSource": "選擇新增來源的方式",
+ "selectedFiles": "已選擇的檔案",
+ "noFilesSelected": "未選擇檔案",
"file": "從檔案",
"remote": "遠端",
"back": "返回",
@@ -220,13 +223,74 @@
},
"progress": {
"upload": "正在上傳",
- "training": "正在訓練",
- "completed": "訓練完成",
+ "training": "正在上傳",
+ "completed": "上傳完成",
"wait": "這可能需要幾分鐘",
- "tokenLimit": "超出令牌限制,請考慮上傳較小的文檔"
+ "preparing": "準備上傳",
+ "tokenLimit": "超出令牌限制,請考慮上傳較小的文檔",
+ "expandDetails": "展開上傳詳情",
+ "collapseDetails": "摺疊上傳詳情",
+ "dismiss": "關閉上傳通知",
+ "uploadProgress": "上傳進度 {{progress}}%",
+ "clear": "清除"
},
"showAdvanced": "顯示進階選項",
- "hideAdvanced": "隱藏進階選項"
+ "hideAdvanced": "隱藏進階選項",
+ "ingestors": {
+ "local_file": {
+ "label": "上傳檔案",
+ "heading": "上傳新文檔"
+ },
+ "crawler": {
+ "label": "爬蟲",
+ "heading": "使用網路爬蟲新增內容"
+ },
+ "url": {
+ "label": "連結",
+ "heading": "從URL新增內容"
+ },
+ "github": {
+ "label": "GitHub",
+ "heading": "從GitHub新增內容"
+ },
+ "reddit": {
+ "label": "Reddit",
+ "heading": "從Reddit新增內容"
+ },
+ "google_drive": {
+ "label": "Google Drive",
+ "heading": "從Google Drive上傳"
+ }
+ },
+ "connectors": {
+ "auth": {
+ "connectedUser": "已連接使用者",
+ "authFailed": "驗證失敗",
+ "authUrlFailed": "取得授權URL失敗",
+ "popupBlocked": "無法開啟驗證視窗。請允許彈出視窗。",
+ "authCancelled": "驗證已取消",
+ "connectedAs": "已連接為 {{email}}",
+ "disconnect": "中斷連接"
+ },
+ "googleDrive": {
+ "connect": "連接到 Google Drive",
+ "sessionExpired": "工作階段已過期。請重新連接到 Google Drive。",
+ "sessionExpiredGeneric": "工作階段已過期。請重新連接您的帳戶。",
+ "validateFailed": "驗證工作階段失敗。請重新連接。",
+ "noSession": "未找到有效工作階段。請重新連接到 Google Drive。",
+ "noAccessToken": "存取權杖不可用。請重新連接到 Google Drive。",
+ "pickerFailed": "無法開啟檔案選擇器。請重試。",
+ "selectedFiles": "已選擇的檔案",
+ "selectFiles": "選擇檔案",
+ "loading": "載入中...",
+ "noFilesSelected": "未選擇檔案或資料夾",
+ "folders": "資料夾",
+ "files": "檔案",
+ "remove": "移除",
+ "folderAlt": "資料夾",
+ "fileAlt": "檔案"
+ }
+ }
},
"createAPIKey": {
"label": "建立新的 API 金鑰",
diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json
index b03bd3e7..c3f4ec59 100644
--- a/frontend/src/locale/zh.json
+++ b/frontend/src/locale/zh.json
@@ -192,6 +192,9 @@
"uploadDoc": {
"label": "上传新文档",
"select": "选择如何将文档上传到 DocsGPT",
+ "selectSource": "选择添加源的方式",
+ "selectedFiles": "已选择的文件",
+ "noFilesSelected": "未选择文件",
"file": "从设备上传",
"back": "后退",
"wait": "请稍等 ...",
@@ -220,13 +223,74 @@
},
"progress": {
"upload": "正在上传",
- "training": "正在训练",
- "completed": "训练完成",
+ "training": "正在上传",
+ "completed": "上传完成",
"wait": "这可能需要几分钟",
- "tokenLimit": "超出令牌限制,请考虑上传较小的文档"
+ "preparing": "准备上传",
+ "tokenLimit": "超出令牌限制,请考虑上传较小的文档",
+ "expandDetails": "展开上传详情",
+ "collapseDetails": "折叠上传详情",
+ "dismiss": "关闭上传通知",
+ "uploadProgress": "上传进度 {{progress}}%",
+ "clear": "清除"
},
"showAdvanced": "显示高级选项",
- "hideAdvanced": "隐藏高级选项"
+ "hideAdvanced": "隐藏高级选项",
+ "ingestors": {
+ "local_file": {
+ "label": "上传文件",
+ "heading": "上传新文档"
+ },
+ "crawler": {
+ "label": "爬虫",
+ "heading": "使用网络爬虫添加内容"
+ },
+ "url": {
+ "label": "链接",
+ "heading": "从URL添加内容"
+ },
+ "github": {
+ "label": "GitHub",
+ "heading": "从GitHub添加内容"
+ },
+ "reddit": {
+ "label": "Reddit",
+ "heading": "从Reddit添加内容"
+ },
+ "google_drive": {
+ "label": "Google Drive",
+ "heading": "从Google Drive上传"
+ }
+ },
+ "connectors": {
+ "auth": {
+ "connectedUser": "已连接用户",
+ "authFailed": "身份验证失败",
+ "authUrlFailed": "获取授权URL失败",
+ "popupBlocked": "无法打开身份验证窗口。请允许弹出窗口。",
+ "authCancelled": "身份验证已取消",
+ "connectedAs": "已连接为 {{email}}",
+ "disconnect": "断开连接"
+ },
+ "googleDrive": {
+ "connect": "连接到 Google Drive",
+ "sessionExpired": "会话已过期。请重新连接到 Google Drive。",
+ "sessionExpiredGeneric": "会话已过期。请重新连接您的账户。",
+ "validateFailed": "验证会话失败。请重新连接。",
+ "noSession": "未找到有效会话。请重新连接到 Google Drive。",
+ "noAccessToken": "访问令牌不可用。请重新连接到 Google Drive。",
+ "pickerFailed": "无法打开文件选择器。请重试。",
+ "selectedFiles": "已选择的文件",
+ "selectFiles": "选择文件",
+ "loading": "加载中...",
+ "noFilesSelected": "未选择文件或文件夹",
+ "folders": "文件夹",
+ "files": "文件",
+ "remove": "删除",
+ "folderAlt": "文件夹",
+ "fileAlt": "文件"
+ }
+ }
},
"createAPIKey": {
"label": "创建新的 API 密钥",
diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx
index 70b9b4f9..6d2223d1 100644
--- a/frontend/src/upload/Upload.tsx
+++ b/frontend/src/upload/Upload.tsx
@@ -1,4 +1,5 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useState } from 'react';
+import { nanoid } from '@reduxjs/toolkit';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -24,6 +25,8 @@ import {
getIngestorSchema,
IngestorOption,
} from '../upload/types/ingestor';
+import { addUploadTask, updateUploadTask } from './uploadSlice';
+
import { FormField, IngestorConfig, IngestorType } from './types/ingestor';
import { FilePicker } from '../components/FilePicker';
@@ -190,12 +193,12 @@ function Upload({
- Choose Files
+ {t('modals.uploadDoc.choose')}
- Selected Files
+ {t('modals.uploadDoc.selectedFiles')}
{files.map((file) => (
@@ -209,7 +212,7 @@ function Upload({
))}
{files.length === 0 && (
- No files selected
+ {t('modals.uploadDoc.noFilesSelected')}
)}
@@ -259,15 +262,8 @@ function Upload({
config: {},
}));
- const [progress, setProgress] = useState<{
- type: 'UPLOAD' | 'TRAINING';
- percentage: number;
- taskId?: string;
- failed?: boolean;
- }>();
-
const { t } = useTranslation();
- const setTimeoutRef = useRef
(null);
+ const dispatch = useDispatch();
const ingestorOptions: IngestorOption[] = IngestorFormSchemas.filter(
(schema) => (schema.validate ? schema.validate() : true),
@@ -279,186 +275,120 @@ function Upload({
}));
const sourceDocs = useSelector(selectSourceDocs);
- useEffect(() => {
- if (setTimeoutRef.current) {
- clearTimeout(setTimeoutRef.current);
- }
+
+ const resetUploaderState = useCallback(() => {
+ setIngestor({ type: null, name: '', config: {} });
+ setfiles([]);
+ setSelectedFiles([]);
+ setSelectedFolders([]);
+ setShowAdvancedOptions(false);
}, []);
- function ProgressBar({ progressPercent }: { progressPercent: number }) {
- return (
-
-
-
-
-
- {progressPercent}%
-
-
-
-
- );
- }
+ const handleTaskFailure = useCallback(
+ (clientTaskId: string, errorMessage?: string) => {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: {
+ status: 'failed',
+ errorMessage: errorMessage || t('attachments.uploadFailed'),
+ },
+ }),
+ );
+ },
+ [dispatch, t],
+ );
- function Progress({
- title,
- isCancellable = false,
- isFailed = false,
- isTraining = false,
- }: {
- title: string;
- isCancellable?: boolean;
- isFailed?: boolean;
- isTraining?: boolean;
- }) {
- return (
-
-
- {isTraining &&
- (progress?.percentage === 100
- ? t('modals.uploadDoc.progress.completed')
- : title)}
- {!isTraining && title}
-
-
{t('modals.uploadDoc.progress.wait')}
-
- {t('modals.uploadDoc.progress.tokenLimit')}
-
- {/*
{progress?.percentage || 0}%
*/}
-
- {isTraining &&
- (progress?.percentage === 100 ? (
-
- ) : (
-
- ))}
-
- );
- }
+ const trackTraining = useCallback(
+ (backendTaskId: string, clientTaskId: string) => {
+ let timeoutId: number | null = null;
- function UploadProgress() {
- return ;
- }
-
- function TrainingProgress() {
- const dispatch = useDispatch();
-
- useEffect(() => {
- let timeoutID: number | undefined;
-
- if ((progress?.percentage ?? 0) < 100) {
- timeoutID = setTimeout(() => {
- userService
- .getTaskStatus(progress?.taskId as string, null)
- .then((data) => data.json())
- .then((data) => {
- if (data.status == 'SUCCESS') {
- if (data.result.limited === true) {
- getDocs(token).then((data) => {
- dispatch(setSourceDocs(data));
- dispatch(
- setSelectedDocs(
- Array.isArray(data) &&
- data?.find(
- (d: Doc) => d.type?.toLowerCase() === 'local',
- ),
- ),
- );
- });
- setProgress(
- (progress) =>
- progress && {
- ...progress,
- percentage: 100,
- failed: true,
- },
- );
- } else {
- getDocs(token).then((data) => {
- dispatch(setSourceDocs(data));
- const docIds = new Set(
- (Array.isArray(sourceDocs) &&
- sourceDocs?.map((doc: Doc) =>
- doc.id ? doc.id : null,
- )) ||
- [],
- );
- if (data && Array.isArray(data)) {
- for (const updatedDoc of data) {
- if (updatedDoc.id && !docIds.has(updatedDoc.id)) {
- dispatch(setSelectedDocs(updatedDoc));
- }
- }
- }
- });
- setProgress(
- (progress) =>
- progress && {
- ...progress,
- percentage: 100,
- failed: false,
- },
- );
- setIngestor({ type: null, name: '', config: {} });
- setfiles([]);
- setProgress(undefined);
- setModalState('INACTIVE');
- onSuccessfulUpload?.();
- }
- } else if (data.status == 'PROGRESS') {
- setProgress(
- (progress) =>
- progress && {
- ...progress,
- percentage: data.result.current,
- },
- );
+ const poll = () => {
+ userService
+ .getTaskStatus(backendTaskId, null)
+ .then((response) => response.json())
+ .then(async (data) => {
+ if (data.status === 'SUCCESS') {
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ timeoutId = null;
}
- });
- }, 5000);
- }
- // cleanup
- return () => {
- if (timeoutID !== undefined) {
- clearTimeout(timeoutID);
- }
+ const docs = await getDocs(token);
+ dispatch(setSourceDocs(docs));
+
+ if (Array.isArray(docs)) {
+ const existingDocIds = new Set(
+ (Array.isArray(sourceDocs) ? sourceDocs : [])
+ .map((doc: Doc) => doc?.id)
+ .filter((id): id is string => Boolean(id)),
+ );
+ const newDoc = docs.find(
+ (doc: Doc) => doc.id && !existingDocIds.has(doc.id),
+ );
+ if (newDoc) {
+ dispatch(setSelectedDocs([newDoc]));
+ }
+ }
+
+ if (data.result?.limited) {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: {
+ status: 'failed',
+ progress: 100,
+ errorMessage: t('modals.uploadDoc.progress.tokenLimit'),
+ },
+ }),
+ );
+ } else {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: {
+ status: 'completed',
+ progress: 100,
+ errorMessage: undefined,
+ },
+ }),
+ );
+ onSuccessfulUpload?.();
+ }
+ } else if (data.status === 'FAILURE') {
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ timeoutId = null;
+ }
+ handleTaskFailure(clientTaskId, data.result?.message);
+ } else if (data.status === 'PROGRESS') {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: {
+ status: 'training',
+ progress: Math.min(100, data.result?.current ?? 0),
+ },
+ }),
+ );
+ timeoutId = window.setTimeout(poll, 5000);
+ } else {
+ timeoutId = window.setTimeout(poll, 5000);
+ }
+ })
+ .catch(() => {
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ timeoutId = null;
+ }
+ handleTaskFailure(clientTaskId);
+ });
};
- }, [progress, dispatch]);
- return (
-
- );
- }
+
+ timeoutId = window.setTimeout(poll, 3000);
+ },
+ [dispatch, handleTaskFailure, onSuccessfulUpload, sourceDocs, t, token],
+ );
const onDrop = useCallback(
(acceptedFiles: File[]) => {
@@ -481,7 +411,7 @@ function Upload({
const doNothing = () => undefined;
- const uploadFile = () => {
+ const uploadFile = (clientTaskId: string) => {
const formData = new FormData();
files.forEach((file) => {
formData.append('file', file);
@@ -489,34 +419,89 @@ function Upload({
formData.append('name', ingestor.name);
formData.append('user', 'local');
+
const apiHost = import.meta.env.VITE_API_HOST;
const xhr = new XMLHttpRequest();
+
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: { status: 'uploading', progress: 0 },
+ }),
+ );
+
xhr.upload.addEventListener('progress', (event) => {
- const progress = +((event.loaded / event.total) * 100).toFixed(2);
- setProgress({ type: 'UPLOAD', percentage: progress });
+ if (!event.lengthComputable) return;
+ const progressPercentage = Number(
+ ((event.loaded / event.total) * 100).toFixed(2),
+ );
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: { progress: progressPercentage },
+ }),
+ );
});
+
xhr.onload = () => {
- const { task_id } = JSON.parse(xhr.responseText);
- setTimeoutRef.current = setTimeout(() => {
- setProgress({ type: 'TRAINING', percentage: 0, taskId: task_id });
- }, 3000);
+ if (xhr.status >= 200 && xhr.status < 300) {
+ try {
+ const parsed = JSON.parse(xhr.responseText) as { task_id?: string };
+ if (parsed.task_id) {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: {
+ taskId: parsed.task_id,
+ status: 'training',
+ progress: 0,
+ },
+ }),
+ );
+ trackTraining(parsed.task_id, clientTaskId);
+ } else {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: { status: 'completed', progress: 100 },
+ }),
+ );
+ onSuccessfulUpload?.();
+ }
+ } catch (error) {
+ handleTaskFailure(clientTaskId);
+ }
+ } else {
+ handleTaskFailure(clientTaskId, xhr.statusText || undefined);
+ }
};
- xhr.open('POST', `${apiHost + '/api/upload'}`);
+
+ xhr.onerror = () => {
+ handleTaskFailure(clientTaskId);
+ };
+
+ xhr.open('POST', `${apiHost}/api/upload`);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
};
- const uploadRemote = () => {
- if (!ingestor.type) return;
+ const uploadRemote = (clientTaskId: string) => {
+ if (!ingestor.type) {
+ handleTaskFailure(clientTaskId);
+ return;
+ }
+
const formData = new FormData();
formData.append('name', ingestor.name);
formData.append('user', 'local');
formData.append('source', ingestor.type as string);
- let configData: any = {};
-
const ingestorSchema = getIngestorSchema(ingestor.type as IngestorType);
- if (!ingestorSchema) return;
+ if (!ingestorSchema) {
+ handleTaskFailure(clientTaskId);
+ return;
+ }
+
const schema: FormField[] = ingestorSchema.fields;
const hasLocalFilePicker = schema.some(
(field: FormField) => field.type === 'local_file_picker',
@@ -528,11 +513,12 @@ function Upload({
(field: FormField) => field.type === 'google_drive_picker',
);
+ let configData: Record = { ...ingestor.config };
+
if (hasLocalFilePicker) {
files.forEach((file) => {
formData.append('file', file);
});
- configData = { ...ingestor.config };
} else if (hasRemoteFilePicker || hasGoogleDrivePicker) {
const sessionToken = getSessionToken(ingestor.type as string);
configData = {
@@ -541,44 +527,122 @@ function Upload({
file_ids: selectedFiles,
folder_ids: selectedFolders,
};
- } else {
- configData = { ...ingestor.config };
}
formData.append('data', JSON.stringify(configData));
const apiHost: string = import.meta.env.VITE_API_HOST;
- const xhr = new XMLHttpRequest();
- xhr.upload.addEventListener('progress', (event: ProgressEvent) => {
- if (event.lengthComputable) {
- const progressPercentage = +(
- (event.loaded / event.total) *
- 100
- ).toFixed(2);
- setProgress({ type: 'UPLOAD', percentage: progressPercentage });
- }
- });
- xhr.onload = () => {
- const response = JSON.parse(xhr.responseText) as { task_id: string };
- setTimeoutRef.current = window.setTimeout(() => {
- setProgress({
- type: 'TRAINING',
- percentage: 0,
- taskId: response.task_id,
- });
- }, 3000);
- };
-
const endpoint =
ingestor.type === 'local_file'
? `${apiHost}/api/upload`
: `${apiHost}/api/remote`;
+ const xhr = new XMLHttpRequest();
+
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: { status: 'uploading', progress: 0 },
+ }),
+ );
+
+ xhr.upload.addEventListener('progress', (event: ProgressEvent) => {
+ if (!event.lengthComputable) return;
+ const progressPercentage = Number(
+ ((event.loaded / event.total) * 100).toFixed(2),
+ );
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: { progress: progressPercentage },
+ }),
+ );
+ });
+
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ try {
+ const response = JSON.parse(xhr.responseText) as { task_id?: string };
+ if (response.task_id) {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: {
+ taskId: response.task_id,
+ status: 'training',
+ progress: 0,
+ },
+ }),
+ );
+ trackTraining(response.task_id, clientTaskId);
+ } else {
+ dispatch(
+ updateUploadTask({
+ id: clientTaskId,
+ updates: { status: 'completed', progress: 100 },
+ }),
+ );
+ onSuccessfulUpload?.();
+ }
+ } catch (error) {
+ handleTaskFailure(clientTaskId);
+ }
+ } else {
+ handleTaskFailure(clientTaskId, xhr.statusText || undefined);
+ }
+ };
+
+ xhr.onerror = () => {
+ handleTaskFailure(clientTaskId);
+ };
+
xhr.open('POST', endpoint);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
};
+ const handleClose = useCallback(() => {
+ resetUploaderState();
+ setModalState('INACTIVE');
+ close();
+ }, [close, resetUploaderState, setModalState]);
+
+ const handleUpload = () => {
+ if (!ingestor.type) return;
+
+ const ingestorSchemaForUpload = getIngestorSchema(
+ ingestor.type as IngestorType,
+ );
+ if (!ingestorSchemaForUpload) return;
+
+ const schema: FormField[] = ingestorSchemaForUpload.fields;
+ const hasLocalFilePicker = schema.some(
+ (field: FormField) => field.type === 'local_file_picker',
+ );
+
+ const displayName =
+ ingestor.name?.trim() || files[0]?.name || t('modals.uploadDoc.label');
+
+ const clientTaskId = nanoid();
+
+ dispatch(
+ addUploadTask({
+ id: clientTaskId,
+ fileName: displayName,
+ progress: 0,
+ status: 'preparing',
+ }),
+ );
+
+ if (hasLocalFilePicker) {
+ uploadFile(clientTaskId);
+ } else {
+ uploadRemote(clientTaskId);
+ }
+
+ handleClose();
+ };
+
const { getRootProps, getInputProps } = useDropzone({
onDrop,
multiple: true,
@@ -731,7 +795,7 @@ function Upload({
/>
- {option.label}
+ {t(`modals.uploadDoc.ingestors.${option.value}.label`)}
@@ -739,18 +803,16 @@ function Upload({
);
};
- let view;
-
- if (progress?.type === 'UPLOAD') {
- view =
;
- } else if (progress?.type === 'TRAINING') {
- view =
;
- } else {
- view = (
+ return (
+
{!ingestor.type && (
- Select the way to add your source
+ {t('modals.uploadDoc.selectSource')}
)}
@@ -768,12 +830,12 @@ function Upload({
alt="back"
className="h-3 w-3 rotate-180 transform"
/>
-
Back
+
{t('modals.uploadDoc.back')}
{ingestor.type &&
- getIngestorSchema(ingestor.type as IngestorType)?.heading}
+ t(`modals.uploadDoc.ingestors.${ingestor.type}.heading`)}
{activeTab && ingestor.type && (
- );
- }
-
- return (
-
{
- close();
- setIngestor({ type: null, name: '', config: {} });
- setfiles([]);
- setModalState('INACTIVE');
- }}
- className="max-h-[90vh] w-11/12 sm:max-h-none sm:w-auto sm:min-w-[600px] md:min-w-[700px]"
- contentClassName="max-h-[80vh] sm:max-h-none"
- >
- {view}
);
}
diff --git a/frontend/src/upload/uploadSlice.ts b/frontend/src/upload/uploadSlice.ts
index 732c69bc..4fd8e1d2 100644
--- a/frontend/src/upload/uploadSlice.ts
+++ b/frontend/src/upload/uploadSlice.ts
@@ -10,12 +10,31 @@ export interface Attachment {
token_count?: number;
}
+export type UploadTaskStatus =
+ | 'preparing'
+ | 'uploading'
+ | 'training'
+ | 'completed'
+ | 'failed';
+
+export interface UploadTask {
+ id: string;
+ fileName: string;
+ progress: number;
+ status: UploadTaskStatus;
+ taskId?: string;
+ errorMessage?: string;
+ dismissed?: boolean;
+}
+
interface UploadState {
attachments: Attachment[];
+ tasks: UploadTask[];
}
const initialState: UploadState = {
attachments: [],
+ tasks: [],
};
export const uploadSlice = createSlice({
@@ -52,6 +71,49 @@ export const uploadSlice = createSlice({
(att) => att.status === 'uploading' || att.status === 'processing',
);
},
+ addUploadTask: (state, action: PayloadAction
) => {
+ state.tasks.unshift(action.payload);
+ },
+ updateUploadTask: (
+ state,
+ action: PayloadAction<{
+ id: string;
+ updates: Partial;
+ }>,
+ ) => {
+ const index = state.tasks.findIndex(
+ (task) => task.id === action.payload.id,
+ );
+ if (index !== -1) {
+ const updates = action.payload.updates;
+
+ // When task completes or fails, set dismissed to false to notify user
+ if (updates.status === 'completed' || updates.status === 'failed') {
+ state.tasks[index] = {
+ ...state.tasks[index],
+ ...updates,
+ dismissed: false,
+ };
+ } else {
+ state.tasks[index] = {
+ ...state.tasks[index],
+ ...updates,
+ };
+ }
+ }
+ },
+ dismissUploadTask: (state, action: PayloadAction) => {
+ const index = state.tasks.findIndex((task) => task.id === action.payload);
+ if (index !== -1) {
+ state.tasks[index] = {
+ ...state.tasks[index],
+ dismissed: true,
+ };
+ }
+ },
+ removeUploadTask: (state, action: PayloadAction) => {
+ state.tasks = state.tasks.filter((task) => task.id !== action.payload);
+ },
},
});
@@ -60,10 +122,15 @@ export const {
updateAttachment,
removeAttachment,
clearAttachments,
+ addUploadTask,
+ updateUploadTask,
+ dismissUploadTask,
+ removeUploadTask,
} = uploadSlice.actions;
export const selectAttachments = (state: RootState) => state.upload.attachments;
export const selectCompletedAttachments = (state: RootState) =>
state.upload.attachments.filter((att) => att.status === 'completed');
+export const selectUploadTasks = (state: RootState) => state.upload.tasks;
export default uploadSlice.reducer;